REST API Reference
Base URL:
https://api.pullpush.ai(prod) ·http://localhost:4264(dev)
Prefer MCP or the dashboard for control-plane work (create connections, mappings,
preview, reconcile, triage). This HTTP reference is intentionally thin: ingest,
webhooks, sandbox, storefront plugins, and automation helpers (operations /
import / export / backup). Full agent surface: MCP tools
and docs/mcp-tools.md.
Authentication
Send a Bearer token unless noted otherwise:
Authorization: Bearer <token>
| Token | Scope | Used for |
|---|---|---|
Tenant API key (pp_…) |
One tenant | Ingest, webhooks setup, imports/exports, backups, sandbox claim, Woo connect |
| Org API key | All tenants in an organization | Same as tenant key where authenticateTenantRoute is used; READONLY org keys get 403 on writes |
| OAuth JWT (ChatGPT / web MCP) | Caller's tenant via membership | POST /mcp only |
Platform admin (MCP_API_KEY) |
All tenants | Trusted internal callers (admin app, MCP admin) |
Some routes accept only a tenant key (exports, backups, Woo connect). Org keys and
MCP_API_KEY are rejected there.
POST /api/v1/events accepts the tenant key via the x-api-key header instead of Bearer.
Discovery
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/.well-known/mcp-server-card.json |
None | MCP server card (tools + streamable HTTP URL) |
GET |
/.well-known/oauth-protected-resource |
None | OAuth protected-resource metadata (RFC 9728) for ChatGPT / remote MCP |
GET |
/api/mcp/info |
Bearer (tenant, org, or admin) | Tool names, descriptions, counts — no full schemas |
Health
GET /health
Liveness check.
{ "ok": true, "service": "kaduno-pullpush-api" }
GET /health/ready
Postgres + Redis readiness. Returns 503 if a dependency is down.
{
"ok": true,
"checks": { "postgres": true, "redis": true },
"service": "kaduno-pullpush-api"
}
Sandbox (try without signup)
POST /api/sandbox
Public. Mints a 48-hour sandbox tenant, a one-time pp_… API key, and two pre-wired
echo connections whose stock snapshots deliberately disagree so a sync preview shows a
real diff. Rate limit: 5 creations per IP per day (global cap on active sandboxes).
Response 201:
{
"ok": true,
"sandbox": true,
"tenant": { "id": "…", "slug": "sandbox-…", "name": "…" },
"apiKey": "pp_…",
"expiresAt": "2026-07-27T00:00:00.000Z",
"mcp": { "remote": { "…": "…" }, "npx": { "…": "…" } },
"preWired": {
"source": { "id": "…", "type": "echo", "direction": "SOURCE" },
"destination": { "id": "…", "type": "echo", "direction": "DESTINATION" }
},
"nextSteps": ["…"]
}
POST /api/sandbox/claim
Convert a sandbox into a persistent free-plan tenant.
Auth: Sandbox tenant API key (Bearer)
Body: { "email": "you@example.com" }
Links an existing user as OWNER, or returns a signupUrl if the email is new.
Event ingest
POST /api/v1/tenants/:tenantId/events
Ingest an event for a tenant. Deduplicated, written to the outbox, and queued.
Auth: Tenant API key (Bearer)
Body:
{
"type": "order.created",
"externalId": "ORD-12345",
"occurredAt": "2026-06-16T10:00:00Z",
"payload": { "orderId": "12345", "total": 599.0, "currency": "NOK" }
}
| Field | Required | Description |
|---|---|---|
type |
Yes | Event type (e.g. order.created, product.updated) |
externalId |
Yes | Unique ID from the source system |
occurredAt |
No | ISO 8601 (defaults to now) |
payload |
No | Event data (defaults to {}) |
Response 200: { "ok": true, "eventId": "…", "requestId": "…" }
Errors: 401 missing_api_key / invalid_tenant_or_key, 400 invalid_body
Deduplication: Same tenant + externalId + type returns the existing event without creating a duplicate.
POST /api/v1/events
Connection-targeted ingest (used by partner connectors such as K-Connect).
Auth: Tenant API key via x-api-key header
Body: Same as above, plus required connectionId.
Response 202: { "eventId": "…" }
Webhooks
POST /api/v1/webhooks/:tenantId/:connectionId
Ingress for external systems. The connector verifies the signature, parses events, and queues them.
Auth: Connector HMAC / signature headers — no Bearer token.
Common headers (connector-specific):
x-wc-webhook-signature(WooCommerce)x-shopify-hmac-sha256(Shopify)x-magento-signature(Magento)x-hub-signature-256/x-hub-signature/x-signature/x-webhook-signature
Response 202: { "ok": true, "eventIds": ["…"], "requestId": "…" }
Replay of a recently seen payload may return 202 with { "replay": true }.
Errors: 404 connection_not_found, 400 connector_has_no_webhook, 401 invalid_signature
API key rotation
POST /api/v1/tenants/:tenantId/keys/rotate
Revokes existing tenant keys and issues a new pp_… key.
Auth: Current tenant API key
Response 200: { "ok": true, "apiKey": "pp_…", "prefix": "pp_abc12", "requestId": "…" }
Store the new key immediately — it cannot be retrieved again.
Flow preview
GET /api/v1/tenants/:tenantId/flows/:flowId/preview
Zero-write sync preview: live snapshots of source + destination in canonical space, then diff. Heavy — use on demand.
Auth: Tenant key, org key, or admin key
Response 200 (SyncPreviewResult):
{
"summary": { "total": 5333, "equal": 103, "changed": 84, "new": 5068, "missing": 78 },
"items": [
{
"key": "BC-1001",
"source": {},
"destination": {},
"diff": "equal|changed|missing_in_dest|missing_in_source"
}
],
"validation": { "source": [], "destination": [] }
}
Async operations
Long-running jobs (reconcile, poll, backfill, etc.) return immediately with an operation id and optional SSE stream.
POST /api/v1/tenants/:tenantId/operations
Auth: Tenant / org / admin key (writes blocked for READONLY org keys)
Body:
{
"type": "reconcile",
"flowId": "…",
"scopeToDestination": true,
"pushDeletes": false
}
type |
Typical extras |
|---|---|
reconcile |
flowId, scopeToDestination?, pushDeletes? |
poll |
connectionId |
deliver-batch |
flowId |
provision |
connector-specific fields |
migration |
migrationId, isFinal? |
export |
connectionId, canonicalType, format, emailTo? |
import-from-connection |
connection/flow fields |
backfill |
since?, until?, limit?, … |
Response 202: { "operationId": "…", "status": "running", "streamUrl": "/api/v1/tenants/…/operations/…/stream" }
GET /api/v1/tenants/:tenantId/operations/:operationId
Poll status.
GET /api/v1/tenants/:tenantId/operations/:operationId/stream
Server-Sent Events progress stream.
File imports
CSV/JSON → canonical → preview → deliver through a flow.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/tenants/:tenantId/imports?canonicalType=stocklevel |
Multipart upload + parse (201) |
PATCH |
/api/v1/tenants/:tenantId/imports/:importId/mapping |
Set { "columnMapping": { … } } |
GET |
/api/v1/tenants/:tenantId/imports/:importId/preview?flowId=… |
Diff against flow destination |
POST |
/api/v1/tenants/:tenantId/imports/:importId/deliver |
Body { "flowId" } — execute |
GET |
/api/v1/tenants/:tenantId/imports/:importId |
Status |
Auth: Tenant / org / admin key (writes require write scope)
Exports
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/tenants/:tenantId/exports |
Start export (202) |
GET |
/api/v1/tenants/:tenantId/exports |
List (?connectionId=) |
GET |
/api/v1/tenants/:tenantId/exports/:exportId |
Status |
GET |
/api/v1/exports/:exportId/download |
Download file |
Auth: Tenant API key only
Start body: { "connectionId", "canonicalType", "format": "csv"|"json"|"email", "emailTo?", "fieldHint?" }
Connector data backups
Tenant-key-only product surface for connection snapshots (scheduling depends on plan / deployment).
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/tenants/:tenantId/backup-policies |
Create/update policy |
GET |
/api/v1/tenants/:tenantId/backup-policies |
List policies |
GET |
/api/v1/tenants/:tenantId/backups |
List snapshots |
POST |
/api/v1/tenants/:tenantId/backup-policies/:policyId/run |
Manual run |
POST |
/api/v1/tenants/:tenantId/backups/:snapshotId/restore |
Restore (destinationConnectionId?) |
GET |
/api/v1/tenants/:tenantId/backups/:snapshotId/download |
Download (gzip JSONL) |
GET |
/api/v1/tenants/:tenantId/backup-usage |
Storage quota |
Policy body: { "connectionId", "canonicalTypes": ["stocklevel"], "cronExpression?", "retentionDays?", "enabled?" }
OAuth2 (dynamic connectors)
For connector definitions with auth.type: "oauth2".
POST /api/v1/oauth/authorize
Auth: Tenant / org / admin key
Body: { "tenantId", "connectorType", "connectionId?", "scopes?", "redirectUri?" }
Response 200: { "ok": true, "redirectUrl": "…", "state": "…" }
GET /api/v1/oauth/callback
Browser callback. Exchanges the code (PKCE when enabled), stores encrypted tokens, redirects to
{ADMIN_URL}/connections?oauth=success&connector={type}.
Storefront plugins
POST /api/connect/woocommerce
One-click handshake used by the WordPress / WooCommerce plugin.
Auth: Tenant API key (Bearer connect token)
Body: WooCommerce credentials (store_url, consumer_key, consumer_secret, webhook_secret, …). Plugin-only fields (platform, plugin_version) are stripped.
Live-tests credentials, creates SOURCE (and DESTINATION if quota allows) connections with envelope-encrypted config.
Response 201:
{
"ok": true,
"connectionId": "…",
"destinationConnectionId": "…",
"webhook_url": "https://api.pullpush.ai/api/v1/webhooks/{tenantId}/{connectionId}",
"webhook_topics": ["order.created", "order.updated", "product.updated"]
}
Shopify
| Method | Path | Purpose |
|---|---|---|
GET |
/api/shopify/install |
Start install (Shopify HMAC; optional ?token= tenant key) |
GET |
/api/shopify/callback |
Complete OAuth, create connection, register webhooks |
POST |
/api/shopify/gdpr |
Mandatory Shopify GDPR webhooks (HMAC on body) |
MCP HTTP
POST /mcp
JSON-RPC 2.0 Streamable HTTP endpoint for MCP tools and widgets. See
MCP tools and the auto-generated
docs/mcp-tools.md.
Auth: Tenant key, org key, admin key, or OAuth JWT (ChatGPT)
Optional headers: x-mcp-scope (operator | dev), x-mcp-workflow-id.
GET /mcp
Returns 405 — clients must use POST (stateless Streamable HTTP).
Alert acknowledge links
One-click links from email notifications (unguessable ids / fingerprints — no API key):
GET /alerts/:id/ackGET /alerts/ack?fp=…&t=…
Common patterns
Request ID
Every request gets a requestId (from X-Request-Id or auto-generated). Included in responses and logs.
Rate limiting
- Global: 120 requests/minute per IP →
429 - Plan limits apply to ingest,
/api/v1/events, and MCP (seeX-RateLimit-*headers)
Errors
{
"error": "error_code",
"details": {}
}
Some partner routes use slightly different message shapes; treat error as the stable code.
Related docs
- Getting started — dashboard path
- Usage guide — day-to-day product usage
- MCP — AI agents / Cursor / Claude
- ChatGPT — connect PullPush in ChatGPT
- WordPress plugin — WooCommerce one-click connect
- Shopify — OAuth install
- FAQ