REST API Reference
Kaduno Pullpush HTTP API. Base URL:
http://localhost:4264(dev) orhttps://api.pullpush.ai(prod).
Authentication
Most endpoints require a Bearer token in the Authorization header:
Authorization: Bearer <token>
| Token type | Scope | Used for |
|---|---|---|
Tenant API key (pp_...) |
Scoped to one tenant | Event ingest, webhooks |
MCP API key (MCP_API_KEY) |
Admin / all tenants | MCP HTTP endpoint |
Health
GET /health
Basic liveness check.
Response 200:
{ "ok": true, "service": "kaduno-pullpush-api" }
GET /health/ready
Readiness check — verifies Postgres and Redis connectivity.
Response 200 (all healthy):
{
"ok": true,
"checks": { "postgres": true, "redis": true },
"service": "kaduno-pullpush-api"
}
Response 503 (dependency down):
{
"ok": false,
"checks": { "postgres": true, "redis": false },
"service": "kaduno-pullpush-api"
}
Event Ingest
POST /api/v1/tenants/:tenantId/events
Ingest an event for a specific tenant. The event is deduplicated, written to the outbox, and queued for processing.
Auth: Tenant API key (Bearer)
Path params:
| Param | Type | Description |
|---|---|---|
tenantId |
string | Tenant UUID |
Body (JSON):
{
"type": "order.created",
"externalId": "ORD-12345",
"occurredAt": "2026-06-16T10:00:00Z",
"payload": {
"orderId": "12345",
"total": 599.00,
"currency": "NOK"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | Event type (e.g. order.created, product.updated) |
externalId |
string | Yes | Unique ID from the source system |
occurredAt |
ISO 8601 | No | When the event happened (defaults to now) |
payload |
object | No | Event data (defaults to {}) |
Response 200:
{
"ok": true,
"eventId": "clxyz...",
"requestId": "uuid"
}
Errors:
| Code | Error | Reason |
|---|---|---|
| 401 | missing_api_key |
No Authorization header |
| 401 | invalid_tenant_or_key |
Tenant not found or key mismatch |
| 400 | invalid_body |
Request body failed validation |
Deduplication: Events are deduplicated on SHA256(tenantId + ":ingest:" + externalId). Sending the same event twice returns the existing eventId without creating a duplicate.
Webhooks
POST /api/v1/webhooks/:tenantId/:connectionId
Receive webhooks from external systems. The connector verifies the signature, parses events, and queues them for processing.
Auth: Signature verification (HMAC) — no Bearer token needed.
Path params:
| Param | Type | Description |
|---|---|---|
tenantId |
string | Tenant UUID |
connectionId |
string | Source connection UUID |
Headers: The connector determines which header to check:
X-Magento-Signature(Magento)X-Signature(generic)X-Hub-Signature(GitHub-style)
Body: Raw webhook payload (connector-specific format).
Response 202:
{
"ok": true,
"eventIds": ["clxyz...", "clabc..."],
"requestId": "uuid"
}
Errors:
| Code | Error | Reason |
|---|---|---|
| 404 | connection_not_found |
Connection doesn't exist or isn't active |
| 400 | connector_has_no_webhook |
Connector type doesn't support webhooks |
| 401 | invalid_signature |
HMAC verification failed |
API Key Rotation
POST /api/v1/tenants/:tenantId/keys/rotate
Rotate the tenant's API key. Revokes all existing keys and issues a new one.
Auth: Current tenant API key (Bearer)
Response 200:
{
"ok": true,
"apiKey": "pp_abc123...",
"prefix": "pp_abc12",
"requestId": "uuid"
}
Important: Store the new key immediately — it cannot be retrieved again.
Flow Preview
GET /api/v1/tenants/:tenantId/flows/:flowId/preview
Zero-write sync preview: snapshot the flow's source + destination in canonical space and diff them. Useful before activating/cutting over a flow. Heavy (live snapshots) — on-demand only.
Auth: Tenant API key (Bearer) or MCP_API_KEY (trusted internal callers, e.g. the admin app).
Response 200: a 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": [] }
}
Errors: 401 missing_api_key / invalid_tenant_or_key, 404 flow_not_found, 500 preview_failed.
OAuth2 Authorization Code Flow
Pullpush supports OAuth2 authorization_code grants for dynamic connectors whose definition
declares auth.type: "oauth2". The flow stores a PKCE-protected state, redirects to the
provider, exchanges the code for tokens, and encrypts them on the connection.
POST /api/v1/oauth/authorize
Initiate an OAuth2 flow. Returns the URL to redirect the user (or browser) to.
Auth: Tenant API key (Bearer) or MCP_API_KEY
Body (JSON):
{
"tenantId": "clxyz...",
"connectorType": "hubspot",
"connectionId": "clxyz...",
"scopes": "crm.objects.contacts.read",
"redirectUri": "https://app.pullpush.ai/api/v1/oauth/callback"
}
| Field | Type | Required | Description |
|---|---|---|---|
tenantId |
string | Yes | Tenant UUID |
connectorType |
string | Yes | Slug of an active OAuth2 connector definition |
connectionId |
string | No | Existing connection to update (omit to create new) |
scopes |
string | No | Override scopes from the definition |
redirectUri |
string | No | Override callback URL (default: {API_BASE_URL}/api/v1/oauth/callback) |
Response 200:
{
"ok": true,
"redirectUrl": "https://provider.example.com/oauth/authorize?...",
"state": "hex-random-state"
}
Errors:
| Code | Error | Reason |
|---|---|---|
| 401 | missing_api_key |
No Authorization header |
| 401 | invalid_key |
Tenant/key mismatch |
| 400 | connector_not_oauth2 |
No active OAuth2 definition for the given type |
| 400 | missing_oauth_config |
Definition missing authorize_url or client_id |
GET /api/v1/oauth/callback
OAuth2 callback. The provider redirects here after user consent. Exchanges the authorization code for tokens (with PKCE if enabled), stores encrypted credentials on the connection, and redirects the user to the admin UI.
Auth: None (validated by matching the state parameter).
Query params:
| Param | Type | Description |
|---|---|---|
code |
string | Authorization code from the provider |
state |
string | State parameter (matched to OAuthState record) |
error |
string | Provider error code (if the user denied consent) |
error_description |
string | Human-readable error |
Success: 302 redirect to {ADMIN_URL}/connections?oauth=success&connector={type}
Errors:
| Code | Error | Reason |
|---|---|---|
| 400 | oauth_provider_error |
Provider returned an error (user denied / config issue) |
| 400 | missing_code_or_state |
Missing code or state query param |
| 400 | invalid_state |
No matching OAuthState record |
| 400 | expired_state |
OAuth flow timed out (10 min TTL) |
| 502 | token_exchange_failed |
Token endpoint returned an error |
Other tenant routes
POST /api/v1/events— generic event ingest used by k-shop K-Connect. Auth viax-api-keyheader (tenant key). Body addsconnectionIdto the ingest shape; returns202 { eventId }.POST /api/v1/tenants/:tenantId/po-desk/*— PO Operations Desk (Linnworks PO → draft → review → accept → stock + Flowretail receivement). Authed like the other tenant routes.
MCP HTTP Endpoint
POST /mcp
JSON-RPC 2.0 endpoint for MCP tool calls. See MCP Tools Reference for available tools.
Auth: Bearer token (MCP_API_KEY for admin, or tenant API key for scoped access)
GET /api/mcp/info
Returns available MCP tool metadata (names, descriptions, schemas).
Response 200:
{
"tools": [
{
"name": "pullpush.tenant.list",
"description": "List all tenants (admin) or the caller tenant.",
"inputSchema": { ... }
}
]
}
Common patterns
Request ID tracing
Every request gets a unique requestId (from X-Request-Id header or auto-generated UUID). It's included in all responses and logs for correlation.
Rate limiting
The API enforces a global rate limit of 120 requests per minute per IP. Returns 429 Too Many Requests when exceeded.
Error format
All errors follow a consistent shape:
{
"error": "error_code",
"details": { ... }
}