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 logsintegrations.install— start OAuth flows, create connectionsintegrations.configure— update connection configintegrations.disconnect— revoke connectionsintegrations.manage_webhooks— CRUD outgoing subscriptions, replay deliveriesintegrations.retry_jobs— retry / cancel queued jobsorders.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 typeX-SoMerch-Delivery— delivery id (unique per attempt-set)X-SoMerch-Timestamp— Unix secondsX-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
GET
application/json/api/public/v1/providersBearerList integration providers
Catalog of provider slugs the platform knows about (hubspot, slack, salesforce, chatgpt-mcp, zapier, generic-webhook, …) with status and supported triggers/actions.
GET
application/json/api/public/v1/connectionsBearerList integration connections for the caller's org
Sanitized view of `integration_connections` for the caller's organization. Never returns credential material.
POST
application/json/api/public/v1/connectionsBearerCreate or update an integration connection
Attach a provider connection to the caller's organization. Use the OAuth flow for hosted providers (see /oauth/{providerSlug}/authorize).
POST
application/json/api/public/v1/oauth/{providerSlug}/authorizeBearerBegin per-tenant OAuth flow
Mints a one-shot state + PKCE verifier and returns the provider's authorization URL. Redirect the user to that URL; the provider will redirect back to /api/public/v1/oauth/{providerSlug}/callback.
GET
text/html/api/public/v1/oauth/{providerSlug}/callbackOAuth callback
Public callback route the provider redirects to. Validates state, exchanges the code, and upserts the connection.
POST
application/json/api/public/v1/actions/{actionKey}BearerInvoke a central action
Central action dispatcher. Supports `Idempotency-Key` header. Built-in actions: system.ping, orders.list, orders.get, quotes.submit, hubspot.contacts.upsert, slack.messages.post, salesforce.leads.create, …
GET
application/json/api/public/v1/eventsBearerList integration events
Normalized event log for the caller's organization. Filter by provider slug, event type, correlation id, resource, or time range.
GET
application/json/api/public/v1/webhooksBearerList outgoing webhook subscriptions
Enumerate the org's outgoing webhook subscriptions.
POST
application/json/api/public/v1/webhooksBearerCreate an outgoing webhook subscription
Register a URL to receive HMAC-SHA256 signed deliveries for the requested event types. The signing secret is returned once at creation time.
GET
application/json/api/public/v1/jobsBearerList queued integration jobs
Inspect background jobs (webhook deliveries, retries, dead-letter).
GET
application/json/api/public/v1/audit-logsBearerSanitized integration audit logs
Cross-provider audit trail. Credentials, tokens, and PII are redacted server-side before writing.
POST
application/json/api/public/webhooks/{providerSlug}/{connectionId}Incoming provider webhook
Verifies the provider signature, normalizes the payload, enqueues a job, and returns 2xx. Configure this URL on the provider side.
POST
application/json/api/public/cron/dispatch-webhooksCron: dispatch pending outgoing webhooks
Secret-gated cron entry point. Called by pg_cron on a fixed schedule to drain `integration_webhook_deliveries` (exponential backoff, dead-letter). Not for direct external use.
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.