Build a reliable ingestion pipeline that verifies Clover order webhooks, enforces idempotency, and writes normalized data into Supabase without race conditions or duplicates.

Clover Webhook Events and Payload Shape

Clover fires merchant.order.created and merchant.order.updated events when an order reaches a terminal state or is modified. The payload contains the order id, merchant id, state, total, and an array of line items; it does not include full customer details or payments, so a follow-up call to the Orders API is usually required for complete records.

Register the webhook in the Clover developer console under the merchant account. Supply a publicly reachable HTTPS URL and select only the two order events. Clover signs each request with an HMAC-SHA256 signature in the X-Clover-Signature header using the webhook secret shown at registration time.

Endpoint Skeleton with Signature Verification

Use a lightweight framework such as Hono on Cloudflare Workers or a Supabase Edge Function. The handler must read the raw body before any JSON parsing, compute the HMAC, and compare it in constant time. Reject any request whose signature does not match; return 200 only after successful processing to avoid Clover retry storms.

import { createHmac, timingSafeEqual } from 'crypto';

export async function POST(req: Request) {
  const body = await req.text();
  const sig = req.headers.get('X-Clover-Signature');
  const hmac = createHmac('sha256', process.env.CLOVER_WEBHOOK_SECRET!);
  hmac.update(body);
  const expected = hmac.digest();
  if (!timingSafeEqual(Buffer.from(sig, 'hex'), expected)) return new Response('invalid', { status: 401 });
  const event = JSON.parse(body);
  // continue
}

Idempotent Upserts with Postgres Constraints

Clover may deliver the same order event multiple times. Store the Clover order id as a unique constraint in Supabase and use an upsert that ignores conflicts. This pattern is identical to the one described in How to Implement Idempotent Webhooks with Postgres Unique Constraints.

Create a table with clover_order_id as primary key and a separate updated_at column that only advances on genuine changes. This prevents unnecessary writes while still allowing line-item reconciliation on updates.

Normalizing Line Items and Payments

After the initial upsert, fetch the full order from Clover’s /v3/merchants/{mId}/orders/{orderId} endpoint using an app token with the required scopes. Map line items into a normalized order_items table that references the parent order. Store payments in a separate order_payments table because a single Clover order can have multiple tenders.

Wrap the entire write sequence in a Supabase transaction or use a database function so that partial failures leave the database in a consistent state. Queue the fetch-and-enrich step with Cloudflare Queues if you expect high order volume.

Error Handling and Retry Semantics

Return 200 only after the transaction commits. On transient Supabase errors, return 503 so Clover retries with exponential backoff. On permanent errors (invalid merchant token, schema violation), log to an observability sink and return 200 to stop retries while surfacing the failure for manual review.

Compare this flow to the Square implementation in How to Sync Square POS Orders to Postgres via Webhooks; the main difference is Clover’s requirement for an additional authenticated fetch and its stricter signature verification.

Production Checklist and Cost Controls

Rotate the Clover webhook secret via environment variables and never log the raw body containing payment data. Monitor the Supabase function invocation count and set alerts on sustained 5xx rates. Because Clover retries aggressively, keep the handler under 2 seconds by moving enrichment work to a queue.

The same webhook-to-Supabase pattern used for Paddle subscriptions in How to Sync Paddle Subscriptions to Supabase via Webhooks applies here; reuse the same idempotency and retry logic rather than building a second implementation.