Skip to content

Developers

Trust Your Move API Documentation

The Trust Your Move API lets your website or other software read live storage availability, units, bookings and invoices, read removals leads and jobs, create storage holds and enquiries, and receive signed webhooks when things change. It is a plain REST API over HTTPS with JSON bodies, versioned at /api/v1.

Version 1 · Last updated 20 October 2026 · Base URL https://trustyourmove.co.uk/api/v1

Overview

Every Trust Your Move firm on the Pro or Enterprise plan can switch on the partner API under Settings → API keys and create keys with the scopes an integration needs. Requests are always scoped to the firm that owns the key: there is no way to read or write another firm’s data, and responses never include client names or email addresses unless the key carries the bookings:read:pii scope.

  • Read-only by default. The default scope is inventory:read. Grant bookings:write only to integrations that need to create bookings. The removals endpoints (leads and jobs) are read-only: nothing can create or change a lead or job through the API.
  • Live data. Availability comes from the same unit state machine the dashboard uses, so what you show on a partner site matches what staff see.
  • Money is integer pence (9500 = £95.00) and dates are ISO-8601 in UTC. The firm’s own timezone is reported in firm.timezone.
  • Removals data. leads:read and jobs:read return enquiries and jobs for firms that have the Leads or Removals module switched on. Jobs carry postcodes, dates and volume but no street addresses, notes, crew or money.
  • Webhooks push booking, invoice, unit, access-code, appointment and removals (lead.created, job.stage_changed, job.completed) events to your server, signed with HMAC-SHA256.

A quick first call once you have a key:

curl https://trustyourmove.co.uk/api/v1/inventory \
  -H "Authorization: Bearer sf_live_YOUR_KEY"

Authentication

Send your key as a bearer token in the Authorization header on every request. Keys look like sf_live_ followed by 43 URL-safe characters. Trust Your Move stores only a SHA-256 hash of the key, so it is shown exactly once when created — if you lose it, revoke it and create another.

Authorization: Bearer sf_live_78NqGf_OEs6LbpsyaQ8jHg9vy8EwZgS…

Scopes

ScopeGrants
inventory:readStorage types, rates, availability, warehouses and units (no client data).
bookings:readBooking references, status, dates and storage type — no client names or emails.
bookings:read:piiAdds client name and email to booking responses. Grant only when needed.
bookings:writeCreate holds and enquiries through POST /api/v1/bookings.
invoices:readInvoice numbers, status, totals and due dates.
leads:readRemovals enquiries with the contact details the enquirer gave (name, email, phone), move postcodes and date, source and status. Never the raw provider payload, IP address or notes.
jobs:readRemovals jobs: reference, stage, type, move postcodes and dates, volume and the client id. No street addresses, notes, crew or money.

Authentication responses

  • 401 unauthorised — no key, a malformed key, an unknown key or a revoked key. The message does not say which.
  • 403 forbidden — the key is valid but the firm has switched the API off, is on a plan without API access, is suspended, or the key lacks the scope the endpoint needs (code: "missing_scope").

Keep keys on your server. Never embed one in a web page, mobile app or public repository — use the public availability endpoint for anything that runs in a browser.

Rate Limits

Each key has its own limit, set when the key is created (default 60 requests per minute, configurable between 10 and 6000). The window is a fixed calendar minute. Every response carries the current state:

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute for this key
X-RateLimit-RemainingRequests left in the current minute
X-RateLimit-ResetUnix timestamp (seconds) when the window resets
Retry-AfterOn a 429 only: seconds to wait before retrying

When you exceed the limit you get 429 rate_limited. Back off until Retry-After has passed; do not retry in a tight loop. The unauthenticated public availability endpoint is limited to 60 requests per minute per IP address instead.

Conventions & Pagination

  • JSON everywhere. Send Content-Type: application/json on POST requests.
  • Field names are snake_case. Enumerations (statuses, billing models, VAT treatments) are upper-case strings exactly as shown in the dashboard.
  • All responses include Cache-Control: no-store and X-API-Version: v1; CORS is allowed for GET requests so a server-rendered partner page can call the API, but keys must still stay server-side.
  • Send OPTIONS to any endpoint for a CORS preflight.

Pagination

List endpoints accept page (from 1) and per (10, 25, 50, 100; default 25) and wrap results in a data array with a pagination object:

{
  "data": [
    "…"
  ],
  "pagination": {
    "page": 2,
    "per": 25,
    "total": 131,
    "pages": 6
  }
}

Results are ordered newest first (bookings, invoices, leads, jobs) or by code (units). Pages are stable within a minute of normal activity; iterate until page === pages.

Errors

Errors always have the same envelope. Validation failures (422) include an issues array with the field path of each problem.

{
  "error": {
    "message": "Validation failed",
    "code": "invalid",
    "issues": [
      {
        "path": "client.email",
        "message": "Invalid email address"
      }
    ]
  }
}
StatusCodeWhen
400bad_requestBody is not valid JSON
401unauthorisedMissing, malformed, unknown or revoked key
403forbidden / missing_scopeAPI off, plan without API, firm suspended, or scope not granted
404not_foundStorage type, warehouse, booking, lead, job or firm not found (within your firm). Leads and jobs also return this when that module is switched off for the firm
409no_availabilityA hold was requested but no unit of that type is free
422invalid / insurance_requiredValidation failed (or an invalid filter on a list); see the message
429rate_limitedOver the key’s per-minute limit
500server_errorSomething failed on our side — safe to retry with the same body

Endpoints

All paths below are relative to https://trustyourmove.co.uk/api/v1.

GET/inventory

Scope required: inventory:read

The firm’s public profile, every active storage type with rates, billing model and live availability, and the list of warehouses. This is the one call most partner sites need.

curl https://trustyourmove.co.uk/api/v1/inventory -H "Authorization: Bearer sf_live_…"
{
  "firm": {
    "slug": "harbourside-storage",
    "name": "Harbourside Storage",
    "tagline": "Secure container storage by the quay",
    "phone": "01752 000111",
    "email": "hello@harbourside.example",
    "website": "https://harbourside.example",
    "address": {
      "line1": "12 Quay Road",
      "line2": null,
      "town": "Plymouth",
      "postcode": "PL4 0AA",
      "country": "GB"
    },
    "booking_url": "https://trustyourmove.co.uk/harbourside-storage",
    "currency": "GBP",
    "timezone": "Europe/London",
    "hold_minutes": 15,
    "insurance_disclosure": {
      "text": "Goods are stored at the owner's risk unless separate insurance is arranged…",
      "version": 1,
      "terms_url": null
    }
  },
  "storage_types": [
    {
      "id": "st_01J…",
      "slug": "35-sq-ft-container",
      "name": "35 sq ft container",
      "description": "Fits the contents of a one-bedroom flat.",
      "size_label": "35 sq ft",
      "dimensions": {
        "width_m": 2.4,
        "length_m": 1.5,
        "height_m": 2.2,
        "volume_m3": 7.9
      },
      "rates": {
        "daily_pence": 400,
        "weekly_pence": 2500,
        "monthly_pence": 9500
      },
      "billing": {
        "model": "RECURRING",
        "payment_mode": "SUBSCRIPTION",
        "deposit_percent": 0,
        "interval": "MONTH",
        "interval_count": 1,
        "interval_label": "monthly",
        "collection_method": "STRIPE_SUBSCRIPTION",
        "vat_treatment": "STANDARD",
        "min_term_days": 0
      },
      "image_url": null,
      "is_active": true,
      "availability": {
        "available": 3,
        "total": 12
      },
      "updated_at": "2026-10-01T08:00:00.000Z"
    }
  ],
  "warehouses": [
    {
      "id": "wh_01J…",
      "code": "QUAY",
      "name": "Quay Road",
      "town": "Plymouth",
      "postcode": "PL4 0AA",
      "country": "GB",
      "is_active": true
    }
  ]
}

insurance_disclosure.text is the wording a client must see before paying; show it and send insurance_acknowledged: true when you create a hold.

GET/inventory/storage-types/{slug}

Scope required: inventory:read

One storage type by its slug (as returned in /inventory), with availability. Returns 404 for inactive or unknown slugs.

curl https://trustyourmove.co.uk/api/v1/inventory/storage-types/35-sq-ft-container -H "Authorization: Bearer sf_live_…"
{
  "storage_type": {
    "id": "st_01J…",
    "slug": "35-sq-ft-container",
    "name": "35 sq ft container",
    "description": "Fits the contents of a one-bedroom flat.",
    "size_label": "35 sq ft",
    "dimensions": {
      "width_m": 2.4,
      "length_m": 1.5,
      "height_m": 2.2,
      "volume_m3": 7.9
    },
    "rates": {
      "daily_pence": 400,
      "weekly_pence": 2500,
      "monthly_pence": 9500
    },
    "billing": {
      "model": "RECURRING",
      "payment_mode": "SUBSCRIPTION",
      "deposit_percent": 0,
      "interval": "MONTH",
      "interval_count": 1,
      "interval_label": "monthly",
      "collection_method": "STRIPE_SUBSCRIPTION",
      "vat_treatment": "STANDARD",
      "min_term_days": 0
    },
    "image_url": null,
    "is_active": true,
    "availability": {
      "available": 3,
      "total": 12
    },
    "updated_at": "2026-10-01T08:00:00.000Z"
  }
}

GET/inventory/availability

Scope required: inventory:read

Counts only — lighter than /inventory for polling.

ParameterTypeDescription
storage_typestringOptional slug to restrict to one type
fromdateOptional ISO date. Adds freeing_by_date: active bookings of that type ending on or before the date.
curl "https://trustyourmove.co.uk/api/v1/inventory/availability?storage_type=locker&from=2026-12-01" -H "Authorization: Bearer sf_live_…"
{
  "as_of": "2026-10-18T09:30:00.000Z",
  "from": "2026-12-01T00:00:00.000Z",
  "storage_types": [
    {
      "id": "st_01J…",
      "slug": "locker",
      "name": "Locker",
      "available": 5,
      "total": 20,
      "by_status": {
        "AVAILABLE": 5,
        "OCCUPIED": 14,
        "UNDER_MAINTENANCE": 1
      },
      "freeing_by_date": 2,
      "rates": {
        "daily_pence": 200,
        "weekly_pence": 1000,
        "monthly_pence": 3000
      }
    }
  ]
}

GET/inventory/units

Scope required: inventory:read

Paginated units with code, status, warehouse and storage type. Units never carry client data, even with the PII scope.

ParameterTypeDescription
statusenumAVAILABLE, RESERVED, OCCUPIED, UNDER_MAINTENANCE, FLAGGED_FOR_DISPOSAL or WRITTEN_OFF
warehousestringWarehouse id or code
storage_typestringStorage type slug
qstringUnit code contains
page, perintPagination
curl "https://trustyourmove.co.uk/api/v1/inventory/units?status=AVAILABLE&warehouse=QUAY&per=50" -H "Authorization: Bearer sf_live_…"
{
  "data": [
    {
      "id": "un_01J…",
      "code": "A-01",
      "status": "AVAILABLE",
      "warehouse": {
        "id": "wh_01J…",
        "code": "QUAY",
        "name": "Quay Road"
      },
      "storage_type": {
        "id": "st_01J…",
        "slug": "35-sq-ft-container",
        "name": "35 sq ft container"
      },
      "last_status_change_at": "2026-10-12T10:15:00.000Z",
      "created_at": "2026-01-04T09:00:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per": 25,
    "total": 48,
    "pages": 2
  }
}

GET/bookings

Scope required: bookings:read

Paginated bookings, newest first. The client object contains only the id unless the key also has bookings:read:pii, in which case name and email are included.

ParameterTypeDescription
statusenumDRAFT, PENDING_PAYMENT, PENDING_REVIEW, CONFIRMED, ACTIVE, PAYMENT_OVERDUE, COMPLETED, CANCELLED, EXPIRED
storage_typestringStorage type slug
referencestringReference contains (e.g. HS-0001)
client_idstringOnly this client
from, todateStart date range
page, perintPagination
curl "https://trustyourmove.co.uk/api/v1/bookings?status=ACTIVE&from=2026-01-01" -H "Authorization: Bearer sf_live_…"
{
  "data": [
    {
      "id": "bk_01J…",
      "reference": "HS-000123",
      "status": "PENDING_PAYMENT",
      "source": "API",
      "storage_type": {
        "id": "st_01J…",
        "slug": "35-sq-ft-container",
        "name": "35 sq ft container"
      },
      "unit": {
        "id": "un_01J…",
        "code": "A-07"
      },
      "client": {
        "id": "cl_01J…"
      },
      "start_date": "2026-11-01T00:00:00.000Z",
      "end_date": null,
      "term_days": null,
      "rate_pence": 9500,
      "rate_basis": "MONTH",
      "billing": {
        "model": "RECURRING",
        "payment_mode": "SUBSCRIPTION",
        "deposit_percent": 0,
        "interval": "MONTH",
        "interval_count": 1,
        "collection_method": "STRIPE_SUBSCRIPTION",
        "vat_treatment": "STANDARD",
        "status": "NONE",
        "next_billing_at": null
      },
      "insurance_acknowledged_at": "2026-10-18T09:30:00.000Z",
      "confirmed_at": null,
      "cancelled_at": null,
      "completed_at": null,
      "created_at": "2026-10-18T09:30:00.000Z",
      "updated_at": "2026-10-18T09:30:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per": 25,
    "total": 1,
    "pages": 1
  }
}

GET/bookings/{id}

Scope required: bookings:read

One booking by id or by reference (case-insensitive, e.g. HS-000123). Same shape and PII rule as the list.

curl https://trustyourmove.co.uk/api/v1/bookings/HS-000123 -H "Authorization: Bearer sf_live_…"

POST/bookings

Scope required: bookings:write

Create a booking on behalf of a client. Two modes:

  • "inquiry" (default) — creates a PENDING_REVIEW booking with no unit. The firm reviews it and contacts the client.
  • "hold" — reserves an available unit of that type for the firm’s hold window (firm.hold_minutes) and returns PENDING_PAYMENT. The client pays on the firm’s booking page (or staff confirm it) before hold_expires_at; otherwise the unit is released automatically. Requires insurance_acknowledged: true.

The client is matched by email within the firm; if none exists one is created (existing records are never overwritten). Bookings created this way are marked source: "API" and trigger booking.created webhooks.

ParameterTypeDescription
storage_typestringRequired. Storage type slug
modeenumhold or inquiry (default)
client.namestringRequired
client.emailstringRequired
client.phone, company_name, is_business, vat_number, address_line1, address_line2, town, postcode, country, marketing_consentvariousOptional; used only when a new client record is created
start_datedateRequired. ISO date, not in the past
term_daysintOptional fixed term (1–3650). Omit for open-ended recurring storage
warehouse_idstringOptional: hold a unit in this warehouse only
insurance_acknowledgedbooleanRequired true for holds; records consent to the firm’s insurance disclosure
notesstringOptional, shown to staff
external_referencestringOptional: your own id, stored in the notes and echoed back
curl -X POST https://trustyourmove.co.uk/api/v1/bookings \
  -H "Authorization: Bearer sf_live_…" \
  -H "Content-Type: application/json" \
  -d '{"storage_type":"35-sq-ft-container","mode":"hold","client":{"name":"Priya Nair","email":"priya@example.com","phone":"07700 900123","postcode":"PL1 2AB"},"start_date":"2026-11-01","insurance_acknowledged":true,"notes":"Moving from a one-bed flat; access needed on Saturdays.","external_reference":"CRM-48213"}'
{
  "booking": {
    "id": "bk_01J…",
    "reference": "HS-000123",
    "status": "PENDING_PAYMENT",
    "source": "API",
    "storage_type": {
      "id": "st_01J…",
      "slug": "35-sq-ft-container",
      "name": "35 sq ft container"
    },
    "unit": {
      "id": "un_01J…",
      "code": "A-07"
    },
    "client": {
      "id": "cl_01J…",
      "name": "Priya Nair",
      "email": "priya@example.com"
    },
    "start_date": "2026-11-01T00:00:00.000Z",
    "end_date": null,
    "term_days": null,
    "rate_pence": 9500,
    "rate_basis": "MONTH",
    "billing": {
      "model": "RECURRING",
      "payment_mode": "SUBSCRIPTION",
      "deposit_percent": 0,
      "interval": "MONTH",
      "interval_count": 1,
      "collection_method": "STRIPE_SUBSCRIPTION",
      "vat_treatment": "STANDARD",
      "status": "NONE",
      "next_billing_at": null
    },
    "insurance_acknowledged_at": "2026-10-18T09:30:00.000Z",
    "confirmed_at": null,
    "cancelled_at": null,
    "completed_at": null,
    "created_at": "2026-10-18T09:30:00.000Z",
    "updated_at": "2026-10-18T09:30:00.000Z"
  },
  "client_created": true,
  "external_reference": "CRM-48213",
  "hold_expires_at": "2026-10-18T09:45:00.000Z",
  "quote": {
    "rate_pence": 9500,
    "rate_basis": "MONTH",
    "term_days": null,
    "due_now_net_pence": 9500,
    "schedule": "Then £95.00 + VAT every month",
    "creates_subscription": true,
    "first_recurring_at": "2026-12-01T00:00:00.000Z"
  },
  "next_step": "Send the client to the firm’s booking page to pay before hold_expires_at, or staff can confirm it in the dashboard."
}

Possible errors: 409 no_availability (hold with nothing free — fall back to an enquiry), 422 insurance_required, 404 not_found for an unknown storage type or warehouse.

GET/invoices

Scope required: invoices:read

Paginated invoice headers with totals and balances. Client and booking are returned as ids only — no names or addresses.

ParameterTypeDescription
statusenumDRAFT, ISSUED, PAID, PARTIALLY_PAID, OVERDUE, VOID, REFUNDED
kindenumBOOKING, DEPOSIT, RECURRING, ADJUSTMENT, CREDIT_NOTE, FINAL
booking_id, client_idstringFilter by booking or client
numberstringInvoice number contains
from, todateIssue date range
page, perintPagination
curl "https://trustyourmove.co.uk/api/v1/invoices?status=OVERDUE" -H "Authorization: Bearer sf_live_…"
{
  "data": [
    {
      "id": "inv_01J…",
      "number": "INV-000412",
      "kind": "RECURRING",
      "status": "PAID",
      "booking_id": "bk_01J…",
      "client_id": "cl_01J…",
      "issue_date": "2026-10-01T00:00:00.000Z",
      "due_date": "2026-10-08T00:00:00.000Z",
      "period_start": "2026-10-01T00:00:00.000Z",
      "period_end": "2026-10-31T00:00:00.000Z",
      "currency": "GBP",
      "subtotal_pence": 9500,
      "vat_pence": 1900,
      "total_pence": 11400,
      "paid_pence": 11400,
      "outstanding_pence": 0,
      "vat_treatment": "STANDARD",
      "paid_at": "2026-10-01T06:02:11.000Z",
      "voided_at": null,
      "created_at": "2026-10-01T06:00:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per": 25,
    "total": 1,
    "pages": 1
  }
}

GET/leads

Scope required: leads:read

Paginated removals enquiries, newest first, with the contact details the enquirer gave, move postcodes and date, source and status. Needs the Leads module to be on for the firm. Repeat enquiries flagged as duplicates are left out unless you ask for them. Never returned: notes, street addresses, the provider’s raw payload, IP address.

ParameterTypeDescription
statusenumNEW, CONTACTED, SURVEY_BOOKED, QUOTED, WON, LOST, SPAM
sourceenumWEBSITE, PHONE, EMAIL, WALK_IN, REFERRAL, REALLYMOVING, PINLOCAL, COMPAREMYMOVE, GETAMOVER, TRIGLOBAL, OTHER_PROVIDER, API, MANUAL
client_idstringOnly leads linked to this client
emailstringExact email match, case-insensitive
created_from, created_todateCreated date range
updated_sincedateOnly leads changed on or after this date
include_duplicatesbooleantrue to include repeat enquiries
page, perintPagination
curl "https://trustyourmove.co.uk/api/v1/leads?status=NEW&created_from=2026-10-01" -H "Authorization: Bearer sf_live_…"
{
  "data": [
    {
      "id": "ld_01J…",
      "status": "NEW",
      "source": "WEBSITE",
      "source_ref": null,
      "client_id": null,
      "first_name": "Priya",
      "last_name": "Nair",
      "email": "priya@example.com",
      "phone": "07700 900123",
      "move_from_postcode": "PL1 2AB",
      "move_to_postcode": "EX4 4QJ",
      "move_date": "2026-11-14",
      "flexible": true,
      "flexible_days": 7,
      "property_type": "TERRACED",
      "bedrooms": 3,
      "estimated_volume_litres": 22000,
      "estimated_volume_m3": 22,
      "volume_basis": "bedrooms_quick_estimate",
      "converted_job_id": null,
      "duplicate_of_id": null,
      "marketing_consent": false,
      "lost_reason": null,
      "created_at": "2026-10-18T09:30:00.000Z",
      "updated_at": "2026-10-18T09:30:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per": 25,
    "total": 1,
    "pages": 1
  }
}

GET/leads/{id}

Scope required: leads:read

One lead by id. Same shape as the list, wrapped in a lead object. Returns 404 if it is not in your firm or the Leads module is off.

curl https://trustyourmove.co.uk/api/v1/leads/ld_01J… -H "Authorization: Bearer sf_live_…"

GET/jobs

Scope required: jobs:read

Paginated removals jobs, newest first. Needs the Removals module to be on for the firm. Archived jobs are left out unless you ask for them. booked_days lists the diary days of the move itself that are not cancelled. Never returned: street addresses, notes, crew, pay rates, prices or costing.

ParameterTypeDescription
stageenumLEAD, SURVEY_BOOKED, SURVEYED, SURVEY_SENT, ESTIMATED, ACCEPTED, DEPOSIT_PAID, CREW_ASSIGNED, JOB_DONE, REVIEWED, BALANCE_PAID, CHASING, IN_STORAGE, LOST, CANCELLED, COMPLETED
typeenumHOME, OFFICE, PART_LOAD, INTERNATIONAL, STORAGE_ONLY, OTHER
client_id, lead_idstringFilter by client or by the lead the job came from
referencestringReference contains (e.g. JOB-0001)
created_from, created_todateCreated date range
move_from, move_todateRequested move date range
updated_sincedateOnly jobs changed on or after this date
include_archivedbooleantrue to include archived jobs
page, perintPagination
curl "https://trustyourmove.co.uk/api/v1/jobs?stage=ACCEPTED&move_from=2026-11-01" -H "Authorization: Bearer sf_live_…"
{
  "data": [
    {
      "id": "jb_01J…",
      "reference": "JOB-000123",
      "stage": "ACCEPTED",
      "type": "HOME",
      "client_id": "cl_01J…",
      "lead_id": "ld_01J…",
      "move_from_postcode": "PL1 2AB",
      "move_to_postcode": "EX4 4QJ",
      "requested_date": "2026-11-14",
      "requested_date_flexible": true,
      "requested_flex_days": 7,
      "requested_window": "ALL_DAY",
      "booked_days": [
        "2026-11-14"
      ],
      "volume_litres": 28000,
      "volume_m3": 28,
      "volume_source": "SURVEY",
      "packing_service": false,
      "dismantling": false,
      "archived": false,
      "satisfaction_score": null,
      "satisfaction_at": null,
      "created_at": "2026-10-18T09:40:00.000Z",
      "updated_at": "2026-10-19T14:05:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per": 25,
    "total": 1,
    "pages": 1
  }
}

GET/jobs/{id}

Scope required: jobs:read

One job by id or by reference (case-insensitive, e.g. JOB-000123). Same shape as the list, wrapped in a job object.

curl https://trustyourmove.co.uk/api/v1/jobs/JOB-000123 -H "Authorization: Bearer sf_live_…"

GET/webhook-events

No API key required

Machine-readable version of the webhooks section: every event name with a description, an example payload, the signature header format and a Node verification snippet. Any valid key works; no scope is required.

curl https://trustyourmove.co.uk/api/v1/webhook-events -H "Authorization: Bearer sf_live_…"

GET/public/availability?firm={slug}

No API key required

An unauthenticated endpoint for availability widgets on partner websites: counts and headline rates per storage type, nothing else. It is rate-limited to 60 requests per minute per IP, cached for 30 seconds and only answers for firms that have the API switched on. Because it needs no key it is safe to call from the browser.

curl "https://trustyourmove.co.uk/api/v1/public/availability?firm=harbourside-storage"
{
  "firm": {
    "slug": "harbourside-storage",
    "name": "Harbourside Storage"
  },
  "as_of": "2026-10-18T09:30:00.000Z",
  "storage_types": [
    {
      "slug": "35-sq-ft-container",
      "name": "35 sq ft container",
      "size_label": "35 sq ft",
      "available": 3,
      "total": 12,
      "rates": {
        "daily_pence": 400,
        "weekly_pence": 2500,
        "monthly_pence": 9500
      }
    }
  ]
}
<div id="trustyourmove-availability"></div>
<script>
fetch('https://trustyourmove.co.uk/api/v1/public/availability?firm=harbourside-storage')
  .then((r) => r.json())
  .then(({ storage_types }) => {
    document.getElementById('trustyourmove-availability').innerHTML = storage_types
      .map((t) => `<p>${t.name}: ${t.available > 0 ? t.available + ' available' : 'Fully booked — enquire'} from £${(t.rates.monthly_pence / 100).toFixed(2)}/month</p>`)
      .join('')
  })
</script>

Webhooks

Webhooks are configured under Settings → Webhooks (Pro and Enterprise). Each endpoint has an HTTPS URL, a list of subscribed events and a signing secret (whsec_…) shown once when the endpoint is created. Trust Your Move queues a delivery for every subscribed event and a background worker POSTs it to your URL within about a minute. The settings page shows every delivery with its response code and body, and lets you retry or send a test.ping.

Events

EventSent when
booking.createdA booking was created (public page, staff or API). Status is PENDING_PAYMENT for a hold or PENDING_REVIEW for an enquiry.
booking.confirmedPayment was taken or staff confirmed the booking; the unit is reserved for the client.
booking.cancelledThe booking was cancelled by staff or the client.
booking.completedThe booking ended and the unit was released.
booking.expiredA hold expired before payment and the unit was released.
invoice.createdAn invoice was issued (booking, deposit, recurring, adjustment, credit note or final).
invoice.paidAn invoice was paid in full (card, bank transfer, cash or cheque).
invoice.payment_failedA card payment attempt failed; dunning may retry.
invoice.voidedAn invoice was voided.
unit.status_changedA storage unit changed status (available, reserved, occupied, maintenance, disposal, written off).
unit.movedA unit was moved IN, OUT or transferred between warehouses.
access_code.createdAn access code or key record was issued to a client.
access_code.revokedAn access code was revoked or expired.
appointment.createdA retrieval or delivery appointment was booked.
appointment.cancelledAn appointment was cancelled.
lead.createdA removals enquiry arrived (website form, lead provider, API or entered by staff). The payload carries ids and the source, not contact details: fetch the lead with the leads:read scope.
job.stage_changedA removals job moved between pipeline stages. The payload carries the job id and reference, the old and new stage and who or what moved it.
job.completedThe work on a removals job is done (the stage reached Job done or Completed for the first time).
test.pingSent from Settings → Webhooks → “Send test event”. Carries no business data.

Payload

Every delivery is a JSON POST with these headers and an envelope containing the delivery id, event name, firm id, event-specific data (ids — fetch full records through the API) and the time the event happened:

HeaderValue
Content-Typeapplication/json
X-StoreFlow-EventEvent name, e.g. booking.confirmed
X-StoreFlow-DeliveryDelivery id — use it to de-duplicate retries
X-StoreFlow-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
{
  "id": "whd_01J9ZKQ4Y4M3V6XQ1W2E3R4T5Y",
  "event": "booking.created",
  "firmId": "firm_01J…",
  "data": {
    "bookingId": "bk_01J9ZKQ4Y4M3V6XQ1W2E3R4T5Z",
    "reference": "HS-000123",
    "status": "PENDING_PAYMENT",
    "clientId": "cl_01J9ZKQ4Y4M3V6XQ1W2E3R4T60",
    "storageTypeId": "st_01J9ZKQ4Y4M3V6XQ1W2E3R4T61",
    "unitId": "un_01J9ZKQ4Y4M3V6XQ1W2E3R4T62"
  },
  "createdAt": "2026-10-18T09:30:00.000Z"
}

Respond with any 2xx status within 10 seconds. Do the real work after responding (queue it) so slow processing never counts as a failure. Deliveries may arrive out of order and, after a retry, more than once — treat them as idempotent using X-StoreFlow-Delivery.

Verifying signatures

The signature is an HMAC-SHA256 over the string <timestamp>.<raw body> using your endpoint’s secret. Verify it on the raw request bytes before parsing JSON, compare with a constant-time function, and reject timestamps more than five minutes old to prevent replays.

import { createHmac, timingSafeEqual } from 'node:crypto'

// Express example: use the RAW body (not the parsed JSON) when computing the HMAC.
app.post('/hooks/trustyourmove', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('x-storeflow-signature') ?? ''        // "t=1760780000,v1=abcdef…"
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const t = Number(parts.t)
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.status(400).send('stale')   // 5-minute tolerance

  const expected = createHmac('sha256', process.env.STOREFLOW_WEBHOOK_SECRET)
    .update(`${t}.${req.body.toString('utf8')}`)
    .digest('hex')
  const ok = expected.length === String(parts.v1).length && timingSafeEqual(Buffer.from(expected), Buffer.from(String(parts.v1)))
  if (!ok) return res.status(400).send('bad signature')

  const event = JSON.parse(req.body.toString('utf8'))        // { id, event, firmId, data, createdAt }
  // handle event.event / event.data — respond quickly, do the work asynchronously
  res.status(200).end()
})

Rotating a secret (Settings → Webhooks → Edit → “Generate a new signing secret”) invalidates the old one immediately, so update your receiver first.

Retries

A non-2xx response, a timeout (10 seconds) or a connection error marks the delivery failed and it is retried after 1, 5, 30, 120 and 720 minutes. After 5 failed attempts it is marked dead and no longer retried automatically; staff can re-queue it from the deliveries log at any time. Disabling an endpoint stops new events being queued without deleting its history.

Changelog

v1 — 20 October 2026

  • Initial public release: inventory, storage types, availability, units, bookings (list, get, create), invoices and webhook-events endpoints.
  • Read-only removals endpoints: leads (list, get) and jobs (list, get), with the leads:read and jobs:read scopes.
  • Scoped API keys with per-key rate limits and X-RateLimit-* headers.
  • Unauthenticated /public/availability widget endpoint.
  • Signed webhooks for 18 events (including lead.created, job.stage_changed and job.completed) plus test.ping, with a five-step retry schedule and a per-endpoint delivery log.

Breaking changes will only ever ship under a new version prefix. Questions or feature requests: use the contact page.