Developer documentation

SoMerch Integrations API

One central HTTP surface under /api/public/v1/* for third-party integrations: per-tenant OAuth, actions, event log, outgoing webhooks, jobs, and audit trail. The machine-readable description lives at /api/public/openapi.json.

Authentication

Every /api/public/v1/* route requires Authorization: Bearer <token>. Two token types are accepted:

  • Supabase user JWT — for user-facing tools acting on behalf of a signed-in SoMerch user. Discover the token endpoint at /.well-known/openid-configuration.
  • Integration API key — long-lived, org-scoped, revocable from the SoMerch admin console. Prefix sk_live_….

Both resolve to a single organization; the caller cannot cross organization boundaries. Missing/invalid tokens return 401 with a WWW-Authenticate header pointing at /.well-known/oauth-protected-resource.

Scopes

Integration API keys carry a set of scopes; user JWTs default to the user's SoMerch role. Requests are rejected with 403 when the caller lacks the scope an action requires.

  • integrations.view — list providers, connections, events, jobs, audit logs
  • integrations.install — start OAuth flows, create connections
  • integrations.configure — update connection config
  • integrations.disconnect — revoke connections
  • integrations.manage_webhooks — CRUD outgoing subscriptions, replay deliveries
  • integrations.retry_jobs — retry / cancel queued jobs
  • orders.read, orders.write, quotes.submit, … — per-domain action scopes

Actions

The central action dispatcher wraps existing SoMerch services so integrations don't duplicate business logic. Call POST /api/public/v1/actions/{actionKey} with a JSON body validated by the action's Zod schema.

curl -X POST https://somerch.co/api/public/v1/actions/quotes.submit \
  -H "Authorization: Bearer $SOMERCH_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f9b…-2a" \
  -d '{
    "company_name": "Acme GmbH",
    "first_name": "Jane",
    "email": "[email protected]",
    "plan_id": "e4b5…",
    "selected_option_id": "balanced",
    "consent_terms": true
  }'

Successful responses are envelope-shaped: { "ok": true, "data": … }. Failures return { "ok": false, "error": { code, message, retryable } } with an appropriate HTTP status.

Events

Every state change in the SoMerch domain (quote submitted, order created, shipment dispatched, inventory changed, …) writes a normalized row to integration_events. Read them with GET /api/public/v1/events; filter by event_type, correlation_id, resource_type, resource_id, or time range.

Event payloads are PII-redacted before being written and before being delivered to outgoing webhooks. If your subscription needs the raw record, call the action or resource endpoint referenced by resource_type / resource_id.

Outgoing webhooks

Register a URL to receive HMAC-signed deliveries for event types your integration cares about:

curl -X POST https://somerch.co/api/public/v1/webhooks \
  -H "Authorization: Bearer $SOMERCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/somerch",
    "event_types": ["quote.submitted", "order.status_changed", "shipment.dispatched"]
  }'

The response body includes the signing secret exactly once — store it. Deliveries arrive as HTTPS POSTs with these headers:

  • X-SoMerch-Event — event type
  • X-SoMerch-Delivery — delivery id (unique per attempt-set)
  • X-SoMerch-Timestamp — Unix seconds
  • X-SoMerch-Signature — sha256=<hex>

Signature verification

Signatures are HMAC-SHA256 over <timestamp>.<raw_request_body> keyed by the subscription secret. Verify with a timing-safe comparison and reject timestamps skewed more than 5 minutes.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: string, headers: Headers, secret: string): boolean {
  const ts  = headers.get("x-somerch-timestamp") ?? "";
  const sig = headers.get("x-somerch-signature") ?? "";
  if (!ts || !sig.startsWith("sha256=")) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`).digest("hex");
  const a = Buffer.from(sig);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

Retries & dead-letter

Failed deliveries (network error, non-2xx response, timeout) are retried up to 8 times with exponential backoff: 30s, 2m, 10m, 30m, 2h, 6h, 12h, 24h. A delivery that returns 429 or supplies a Retry-After header is rescheduled to honor it.

After 8 failed attempts the delivery is moved to dead-letter and the subscription is auto-disabled after 20 consecutive failures. Replay any delivery manually from the admin UI or with POST /api/public/v1/webhooks/deliveries/{id}/replay.

Idempotency

All state-changing actions accept an Idempotency-Key header. Repeated requests with the same key within 24 hours return the original response instead of re-running the action. Keys are scoped to (caller, action_key).

Incoming provider webhooks are additionally deduplicated by (provider_slug, connection_id, external_event_id), so providers that retry deliveries never double-process on our side.

Rate limits

Default limits per connection: 60 requests / minute and 10,000 requests / day. Rate-limited responses return 429 with Retry-After. Contact support to raise limits for a specific integration.

Error model

All errors follow a single envelope. HTTP status reflects the category; error.code is a stable machine key; error.retryable tells you whether to retry.

{
  "ok": false,
  "error": {
    "code": "validation_error",     // stable machine key
    "message": "email is required", // human-readable
    "retryable": false,             // safe to retry as-is?
    "field_issues": [               // present on validation errors
      { "field": "email", "message": "Required" }
    ]
  }
}

Common codes: validation_error, unauthorized, forbidden, not_found, rate_limited, idempotency_conflict, provider_error, pipeline_error, internal_error.

Endpoint reference

Changelog

  • 2026-07-07 — v1 launched. Per-tenant OAuth for HubSpot, Slack, Salesforce; central action dispatcher; outgoing webhooks with HMAC + retries + dead-letter; unified event log; admin console under /admin/integrations.