Public REST API (v1)
Call Meridian Cloud from your own systems — read and write your customers, jobs, quotes, and invoices with an API key.
Authentication#
Generate a key under Settings → API & Webhooks → “Generate new key.” The full key is shown exactly once — copy it somewhere safe; Meridian only ever stores a hash of it, so it can’t be shown again. If you lose it, revoke it and generate a new one.
Send it on every request as a Bearer token: `Authorization: Bearer mk_live_...`. A key grants full access to your workspace’s data through the API — there’s no separate read/write scoping yet. Revoke a key any time from the same settings page; a revoked key stops working immediately.
Base URL#
https://go.meridiancloud.app/api/v1
Endpoints (v1)#
Customers — `GET /customers` (list), `GET /customers/{id}`, `POST /customers` (create).
Jobs — `GET /jobs` (list), `GET /jobs/{id}`, `POST /jobs` (create).
Invoices — `GET /invoices` (list), `GET /invoices/{id}`. Read-only in v1 — invoices are created through Meridian’s own quoting/job flows, not the API.
Quotes — `GET /quotes` (list), `GET /quotes/{id}`. Also read-only in v1.
Every request and response is JSON. A `POST` returns the created resource with a `201` status.
Public form submissions (no key)#
Already have a website you love? Keep it. `POST https://go.meridiancloud.app/api/public/forms/submit` lets an external site you run deliver submissions into any form you’ve made public on your mini-site — the same validation, spam throttling, and automations as if the visitor had submitted it on your Meridian site.
Body: `{ "tenantSlug": "your-workspace-slug", "formSlug": "<the form’s public URL slug>", "data": { ... }, "clientToken": "<optional>" }`. The keys and allowed values inside `data` are exactly the field keys the form defines in Settings → Forms.
No API key is needed — the endpoint can only reach forms that are already publicly submittable at `/f/{formSlug}` on your site, and it enforces the same per-IP rate limit those forms have. Repeated posts with the same `clientToken` are accepted once — safe to retry.
Statuses: `200` accepted, `404` unknown workspace or form, `422` validation failed (per-field messages in `errors`), `429` rate limited.
Pagination#
List endpoints accept `?limit=` (default 50, maximum 100) and `?cursor=`. Every list response has the shape `{ "data": [...], "next_cursor": "..." }` — pass `next_cursor` back as `?cursor=` to fetch the next page. `next_cursor` is `null` on the last page.
Errors#
Every error response has the shape `{ "error": { "code": "...", "message": "..." } }`. Common statuses: `401` (missing, invalid, or revoked key), `404` (the resource doesn’t exist, or belongs to a different workspace — the API never discloses which), `422` (the request body is missing a required field or has an invalid value), `429` (rate limited — see below), `500` (something went wrong on our end).
Rate limits#
Requests are limited to 120 per 60 seconds, per API key and, independently, per source IP — whichever limit is hit first applies. If you’re rate limited you’ll get a `429` — back off and retry after a short delay. Contact support if your integration needs a higher limit.
Webhooks#
Create an endpoint under Settings → API & Webhooks → “Add endpoint”: give it an https URL and pick which events it should receive (or subscribe to every event with the `*` wildcard). Meridian shows a signing secret once, at creation — copy it somewhere safe; it’s shown again any time from the same settings page, and is used only to verify deliveries, never sent anywhere itself.
Every event Meridian emits internally (job created, quote sent, invoice paid, and the rest of the automation trigger catalog you see in Settings → Automations) is delivered to every enabled endpoint subscribed to it. Delivery is best-effort — one attempt per event, with a short timeout — and each endpoint’s settings row shows its most recent success and failure time so you can see at a glance whether deliveries are landing.
For safety, every delivery resolves your endpoint’s hostname and confirms the address it actually resolves to before connecting — an endpoint whose URL resolves to a loopback, private, link-local, or other internal/cloud-metadata address is blocked automatically, even if the hostname itself looks public.
Each delivery is a `POST` with a JSON body of the shape `{ "id": "...", "event": "job.created", "created": 1717000000, "data": { ... } }` — `id` is a unique delivery id, `event` is the event key that fired, `created` is a Unix timestamp (seconds), and `data` is the event’s payload (the same payload shape automations receive for that event).
Verifying a webhook signature#
Every delivery carries three headers: `X-Meridian-Signature: t=<unix seconds>,v1=<hex-encoded HMAC>`, `X-Meridian-Event: <event key>`, and `X-Meridian-Delivery: <delivery id>`.
To verify a delivery, recompute `HMAC-SHA256(your_signing_secret, "{t}.{raw request body}")` using the `t` value from the header — over the EXACT raw bytes you received, before any JSON parsing — and compare it to the `v1` value using a constant-time comparison, never `===`/`==`. Reject the request if the signatures don’t match.
Also reject the request if `t` is too old — a few minutes of tolerance is reasonable — to protect against a captured request being replayed later. A correctly-signed but stale timestamp should be treated the same as an invalid signature.
Respond quickly with any `2xx` status once you’ve verified and durably queued the event; anything else (including a timeout) is recorded as a failed delivery. There’s no automatic retry in this version — a slow or unreachable endpoint simply misses that delivery.
Related articles