API Reference
Generated from the controllers and Form Requests under backend/app/Http/*/Api/V1/. Use php artisan openapi:generate to produce the machine-readable OpenAPI 3 JSON at storage/app/api-docs/openapi.json, served from /docs/openapi.json.
Base URL: /api/v1
Content type: application/json
Auth: Authorization: Bearer <sanctum-token> (where applicable)
Conventions
- Lists are paginated Laravel-style:
{ data, links, meta }. - Single resources are wrapped:
{ data: { … } }. - Errors follow
{ message, errors }—422for validation,404for missing,403for tenant issues.
System
GET /system/health
Health check. Public.
Response
{
"service": "vetty-api",
"status": "ok",
"env": "local",
"clients": ["clinic_saas_web", "pet_owner_mobile", "tenant_scoped_api"]
}
Auth
POST /auth/login
Body
| Field | Type | Rules |
|---|---|---|
email | string | required, email |
password | string | required, min:8 |
Response — UserResource with eager-loaded current_tenant and tenants.
POST /auth/owner/register
Body
| Field | Type | Rules |
|---|---|---|
name | string | required |
email | string | required, unique:users |
password | string | required, min:8, confirmed |
phone | string | nullable |
tenant_id | integer | required, exists:tenants,id |
Response
{
"data": {
"user": { /* UserResource */ },
"client_id": 42
}
}
Tenants
POST /tenants
Provisions a new clinic and its admin user.
Body
| Field | Type | Rules |
|---|---|---|
name | string | required |
slug | string | required, unique:tenants |
email | string | required, unique:tenants |
phone | string | nullable |
address | string | nullable |
subscription_plan | enum | optional, one of trial,starter,growth,premium,enterprise |
owner_name | string | required |
owner_email | string | required, unique:users |
password | string | required, min:8 |
Response
{
"data": {
"tenant": { /* TenantResource */ },
"user": { /* UserResource */ }
}
}
Clinic (tenant-scoped)
All endpoints in this block are prefixed with /tenant/{tenant} and wrapped in ResolveTenantMembership.
GET /tenant/{tenant}/dashboard
Returns { tenant: {…}, stats: { pets_count, clients_count, appointments_count, pending_invoices_count } }.
GET /tenant/{tenant}/clients
Paginated list of ClientResource (50 per page).
GET /tenant/{tenant}/pets
Paginated list of PetResource (20 per page), with client preloaded.
POST /tenant/{tenant}/pets
Body: see StorePetRequest in Requests & Resources.
Response: 201 PetResource with loaded client.
POST /tenant/{tenant}/patients/onboard
Same as POST /pets above — alias endpoint used by the owner-portal onboarding flow.
PATCH /tenant/{tenant}/patients/{pet}
Partial update of a pet. All fields optional; rules follow UpdatePatientDetailsRequest.
GET /tenant/{tenant}/appointments
Paginated (20 per page). AppointmentResource with pet, client, veterinarian eager-loaded.
POST /tenant/{tenant}/appointments
Body: StoreAppointmentRequest. 201 with AppointmentResource.
PATCH /tenant/{tenant}/appointments/{appointment}
Body
| Field | Type | Rules |
|---|---|---|
status | enum | required, one of scheduled,confirmed,checked_in,completed,cancelled,no_show |
Only the status is patched.
GET /tenant/{tenant}/invoices
Paginated (20 per page). Each InvoiceResource has pet, client, payments preloaded.
POST /tenant/{tenant}/invoices
Body: StoreInvoiceRequest. If payment_amount is included, a Payment is created and the invoice status + balance_due are updated.
POST /tenant/{tenant}/invoices/{invoice}/payments
Body
| Field | Type | Rules |
|---|---|---|
amount | number | required, > 0 |
payment_method | enum | required, one of cash,card,upi,bank_transfer,online |
reference_number | string | nullable |
Creates the Payment, subtracts from invoice.balance_due, flips invoice.status to partial or paid.
Owner portal
All /owner/me/* endpoints scope on the authenticated user — the bearer token identifies the owner; no tenant routing is required.
GET /owner/me/pets
Returns paginated PetResource for pets whose client row has owner_user_id = $request->user()->id.
POST /owner/me/pets
Create a pet under the owner's clinic (the first Client row linking the owner to a tenant).
Body (see StoreOwnerPetRequest)
| Field | Type | Rules |
|---|---|---|
name | string | required |
species | string | required |
breed | string | nullable |
sex | enum | nullable, male|female|unknown |
date_of_birth | date | nullable |
color | string | nullable |
microchip_id | string | nullable |
Response — 201 PetResource.
PATCH /owner/me/pets/{pet}
Partial update. The backend verifies the pet belongs to the authenticated owner. Fields identical to the store request; all optional.
GET /owner/me/pets/{pet}/vaccinations
List vaccination records for the owned pet. Returns VaccinationResource collection (not paginated — usually small).
POST /owner/me/pets/{pet}/vaccinations
Body
| Field | Type | Rules |
|---|---|---|
name | string | required |
administered_at | date | required |
next_due_at | date | nullable |
notes | string | nullable |
lot_number | string | nullable |
Response — 201 VaccinationResource.
GET /owner/me/appointments
Returns paginated AppointmentResource where the pet's client belongs to the owner.
GET /owner/me/invoices
Returns paginated InvoiceResource filtered by owner.
POST /owner/me/invoices/{invoice}/razorpay/order
Create a Razorpay order for an unpaid invoice the owner owns. The vetty app passes the returned payload straight into the Razorpay Flutter checkout SDK.
Response
{
"data": {
"invoice_id": 42,
"key": "rzp_test_XXXXXXXXXXXXXX",
"order_id": "order_Nabc123def456",
"amount": 125000,
"currency": "INR",
"name": "Paws & Claws Veterinary",
"description": "Invoice INV-2026-00042",
"prefill": { "name": "Asha Rao", "email": "asha@example.com", "contact": "+919999999999" }
}
}
amount is in the smallest currency unit (paise for INR). Returns 409 if the invoice is already paid / voided / an estimate, 503 if Razorpay keys aren't configured, and 404 if the authenticated user doesn't own the invoice.
POST /owner/me/invoices/{invoice}/razorpay/verify
Called by the vetty app after the Razorpay SDK reports success. Re-checks the HMAC server-side, records a captured Payment row, and recomputes balance_due / status from the ledger.
Body
| Field | Type | Rules |
|---|---|---|
razorpay_order_id | string | required — the order id from the previous call |
razorpay_payment_id | string | required — returned by the Razorpay SDK on success |
razorpay_signature | string | required — the HMAC the SDK returns |
Returns 422 on signature mismatch, 200 with the updated InvoiceResource on success. Idempotent: the unique index on provider_payment_id means a webhook that raced the client-verify won't double-bill.
POST /webhooks/razorpay
Public (no auth) webhook receiver. Verifies the X-Razorpay-Signature header against RAZORPAY_WEBHOOK_SECRET and handles payment.captured / payment.failed. Returns 500 on handler errors so Razorpay retries.
GET /owner/me/shares
List active share codes the owner has generated.
POST /owner/me/shares
Create a new share code for one of the owner's pets.
Body
| Field | Type | Rules |
|---|---|---|
pet_id | integer | required, must belong to the owner |
note | string | nullable (free-text reminder to the owner) |
expires_in_minutes | integer | nullable, 5–1440; defaults to the app's configured TTL |
Response
{
"data": {
"id": 17,
"code": "A1B2C3D4",
"pet_id": 5,
"expires_at": "2026-04-19T15:30:00Z",
"note": "Handoff to Paws & Claws"
}
}
POST /owner/me/shares/{share}/revoke
Marks the share as revoked so vetdoctor can no longer redeem it. 200 with updated PetShareResource.
Providers directory
GET /providers
Public search endpoint used by vetty's Nearby tab to discover clinics, groomers, breeders and trainers.
Query
| Field | Type | Notes |
|---|---|---|
q | string | Free-text match on tenant name / description |
business_type | enum | clinic|groomer|breeder|trainer|all |
latitude | float | Requires longitude + radius_km |
longitude | float | Requires latitude + radius_km |
radius_km | float | km radius for the haversine filter |
Response: paginated TenantResource with each tenant's public profile fields.
GET /providers/{tenant}
Public profile for a single provider — the resolved TenantResource. Used by the vetty app when the user taps a result.
Shares (cross-app)
POST /shares/redeem
Called by vetdoctor after scanning a QR. Validates the share code and reparents the pet onto the scanning clinic's tenant (a new Client row linking the owner to the target tenant is created if none exists).
Body (see RedeemPetShareRequest)
| Field | Type | Rules |
|---|---|---|
code | string | required, 8 chars, must be active and unexpired |
tenant_id | integer | required, must be the scanning clinic's tenant |
user_id | integer | required, the scanning clinic user |
Response — 200 { data: { pet: PetResource, client_id, message } }.
GET /shares/{code}
Peek at a share code without redeeming it (used by the scan flow to preview before confirming). Returns the target pet + owner name but does not mutate anything.
Health journal
Lightweight pet-observation log. Owners write weights, appetite / energy / stool grades, symptom tags, and notes. Clinicians read the same log during consultation (clinic-sourced entries are distinguishable via source: 'clinic').
GET /owner/me/pets/{pet}/health-logs
Lists entries newest first. Optional ?since=YYYY-MM-DD filters to entries logged on or after that date. Returns a paginated PetHealthLogResource collection.
POST /owner/me/pets/{pet}/health-logs
Body (see StorePetHealthLogRequest). At least one of weight_kg, appetite, energy_level, stool_quality, water_intake_ml_per_kg, symptoms, or notes must be present.
| Field | Type | Rules |
|---|---|---|
logged_at | datetime | optional, defaults to now() |
weight_kg | decimal | optional, ≤ 9999.99 |
appetite | enum | normal|reduced|none|increased |
energy_level | enum | normal|low|lethargic|hyperactive |
stool_quality | enum | normal|soft|diarrhea|constipated|bloody |
water_intake_ml_per_kg | int | optional, 0–500 |
symptoms | string[] | e.g. ['Coughing', 'Limping'] |
notes | string | free text, ≤ 2000 chars |
When the new weight_kg is the freshest reading, the controller bubbles it onto pets.weight_kg so dashboards and passport PDFs pick it up without a join.
DELETE /owner/me/pets/{pet}/health-logs/{healthLog}
Soft-constraint: only the author can delete (logged_by_user_id must match $request->user()). 204 on success, 404 if the entry belongs to another writer.
Clinic-side
GET /tenant/{tenant}/patients/{pet}/health-logs (read) and POST /tenant/{tenant}/patients/{pet}/health-logs (write) mirror the owner routes, gated on medical_record.read / .create permissions. The controller auto-detects source from the path ('clinic' when the request path contains /tenant/).
Lost-pet public tag
POST /owner/me/pets/{pet}/public-profile
Primary-owner-only. Enables the public profile (minting an 8-char slug on first call) and stores the share-able fields.
| Field | Type | Rules |
|---|---|---|
microchip_id | string | optional, ≤ 40 |
owner_contact_phone | string | optional, ≤ 40 |
reward_notes | string | optional, ≤ 1000 |
Response — { data: { slug, enabled, public_url, microchip_id, owner_contact_phone, reward_notes, lost_mode, lost_since } }.
DELETE /owner/me/pets/{pet}/public-profile
Disables the tag (slug is retained for later re-enable). Also clears lost_mode_enabled_at.
POST /owner/me/pets/{pet}/public-profile/lost
Marks the pet as actively missing. Auto-enables the tag if it wasn't already. Response includes the public_url to share.
DELETE /owner/me/pets/{pet}/public-profile/lost
Marks the pet as found. Sets lost_mode_enabled_at = null.
GET /p/{slug} (public, HTML)
Public-facing landing page. Mobile-first layout, pet photo, species/breed, microchip ID (if opted in), reward notes, 📞 Call owner button.
GET /api/v1/p/{slug} (public, JSON)
Same data as the HTML view but as JSON — for clients that want to render their own UI on top of a scan.
Pet documents
Per-pet file locker (receipts, lab reports, X-rays, insurance, prescriptions, other). Files live on the private local disk; downloads go through short-lived signed URLs.
GET /owner/me/pets/{pet}/documents
Optional ?category= filter on receipt|lab_report|xray|insurance|prescription|passport|other. Paginated PetDocumentResource collection — each row carries a fresh signed download_url that is valid for 5 minutes from the time of the response.
POST /owner/me/pets/{pet}/documents (multipart)
Upload a file. Primary owner or full-access caregiver only.
| Field | Type | Rules |
|---|---|---|
file | file | required, ≤ 10MB, mimes: pdf/jpg/jpeg/png/heic/webp/doc/docx |
category | enum | see above; default other |
title | string | optional, ≤ 180 |
document_date | date | optional |
notes | string | optional, ≤ 2000 |
GET /owner/me/pets/{pet}/documents/{document}
Returns a single PetDocumentResource.
GET /owner/me/pets/{pet}/documents/{document}/download?signature=...
Serves the raw file. Gated on Laravel's signed middleware — the signature alone authorises the download, so this link is shareable (briefly) without a bearer token. Controller re-verifies that the document belongs to the supplied pet.
DELETE /owner/me/pets/{pet}/documents/{document}
Removes both the database row and the underlying file. Primary owner or full-access caregiver.
GET /owner/me/pets/{pet}/passport.pdf
Generates a pet passport PDF on demand (identification band + owner + vaccination history, formatted for airlines / boarding kennels). No caching — always reflects the latest vaccination list. Returns application/pdf.
Caregivers
Primary owners can invite family or sitters as caregivers on a pet record. Invitees receive an email with a token; once redeemed, they can read (or read+write, depending on access_level) the pet without sharing the owner's credentials.
GET /owner/me/pets/{pet}/caregivers
Primary-owner-only. Returns all caregiver rows (active, pending, expired, revoked) with status_label and access_level.
POST /owner/me/pets/{pet}/caregivers
| Field | Type | Rules |
|---|---|---|
invited_email | string | required, email, not the owner's own email |
invited_name | string | optional, ≤ 180 |
access_level | enum | required, view|full |
note | string | optional, shown in the invite email |
Dispatches SendCaregiverInviteEmailJob via the notification driver. Returns a PetCaregiverResource with the newly minted invite.
POST /owner/me/pets/{pet}/caregivers/{caregiver}/revoke
Primary-owner-only. Sets revoked_at = now(). 200 on success.
POST /caregivers/accept (auth:sanctum)
Invitee endpoint. Body: { "token": "<48-char token>" }. Returns 404 on bad token, 410 on revoked / expired, 409 on already-accepted. On success, binds caregiver_user_id to the authenticated user and returns { pet: PetResource, access_level }.
GET /owner/me/caregivers/shared-with-me
Invitee-side view — lists pets that have been shared with the authenticated user (accepted + not revoked). Powers the "Shared with me" section in the vetty Pets tab.
Marketplace order requests
Owners ping a pharmacy or retail tenant with a refill, a reorder, or a custom item request. The seller accepts and fulfils it outside the app (pickup or delivery); there is no in-app cart or payment — the seller drops a regular invoice via the normal billing flow when the order is ready, and the customer pays via Razorpay as with any other invoice.
GET /owner/me/orders
Lists every order the signed-in owner (or their caregiver) has placed. Optional ?status=pending|accepted|fulfilled|rejected|cancelled and ?pet_id=123 filters. Paginated (30 per page), newest first.
GET /owner/me/pets/{pet}/prescriptions
All prescriptions issued against the pet across tenants. Used by the vetty UI to render the refill picker.
GET /owner/me/pets/{pet}/prescriptions/{prescription}/refill-draft
Returns a pre-composed { kind: 'refill', source_prescription_id, item_summary, notes, pet_id } derived from the prescription's line items — the client merges in target_tenant_id, delivery choice, and submits.
POST /owner/me/pets/{pet}/orders
| Field | Type | Rules |
|---|---|---|
target_tenant_id | int | required, must belong to a pharmacy or retail tenant |
kind | enum | required, refill | reorder | custom |
source_prescription_id | int | required when kind=refill; must belong to this pet |
item_summary | string | required for reorder/custom, optional for refill (derived) |
delivery_preference | enum | required, pickup | delivery |
delivery_address | string | required when delivery_preference=delivery |
notes | string | optional, max 2000 |
Creates the order with status pending and dispatches SendOrderRequestEmailJob to the seller's contact email. Returns an OrderRequestResource.
GET /owner/me/orders/{order}
Fetches one order with pet, target_tenant, source_prescription.items, and assignee loaded. Only the requesting user can read their own orders (404 otherwise).
POST /owner/me/orders/{order}/cancel
Owner bails out before fulfilment. 422 if the order is already fulfilled / rejected / cancelled; otherwise flips status to cancelled and stamps cancelled_at.
GET /api/v1/tenant/{tenant}/orders (seller)
Inbox for pharmacy / retail tenants. Gated on order.read. Default filter is the open queue (status in (pending, accepted)); ?status=all returns history, ?status=<specific> pins to one.
POST /api/v1/tenant/{tenant}/orders/{order}/accept (seller)
Pending → accepted. Gated on order.manage. Stamps accepted_at and records the handling user as assigned_to_user_id.
POST /api/v1/tenant/{tenant}/orders/{order}/reject (seller)
Pending → rejected. Gated on order.manage. Body: { "reason": "..." } (optional). Reason is shown to the owner in the vetty order detail.
POST /api/v1/tenant/{tenant}/orders/{order}/fulfill (seller)
Accepted → fulfilled. Gated on order.manage. Terminal — the owner received the items; further changes (refunds, returns) happen via the regular invoice surface, not this row.
Permissions added to PermissionRegistry:
order.read— View incoming order requestsorder.manage— Accept, reject, or fulfil incoming order requests
Seeded on the tenant_admin (via *), pharmacy, and retail_counter starter roles. A clinic-only tenant has no roles with these permissions by default — reasonable since clinics don't fulfil pharmacy orders.
Error shape
All endpoints return errors in a consistent shape:
{
"message": "The given data was invalid.",
"errors": {
"email": ["The email must be a valid email address."]
}
}
See app/OpenApi/OpenApiSpec.php → ErrorResponse schema.
Swagger / OpenAPI
Generate + serve:
php artisan openapi:generate
# outputs storage/app/api-docs/openapi.json
# exposed at
GET /docs/openapi.json
Load the JSON into any Swagger / Redoc viewer for an interactive tryout.