Event API
The Event API is how an external system pushes events into a connector in
real time: POST /api/v1/events accepts one business event, persists it, and
enqueues it for the flow worker. Flows subscribe to (source: "event_api", event_type) pairs — see the
flow authoring guide.
This page covers the real-time path. For day-0 loads and large re-syncs (thousands of records in one shot) use the bulk import endpoint instead.
Authentication
Every request carries a connector API key in the x-api-key header. Keys
are scoped to one connector, look like ps_live_..., and are minted from the
dashboard (connector → Settings → API keys) or via the API:
| Method | Path | Effect |
|---|---|---|
POST | /api/connectors/{connector_id}/api-keys | Create a key. The full secret is returned once, at creation. |
GET | /api/connectors/{connector_id}/api-keys | List keys (prefix + metadata only). |
DELETE | /api/connectors/{connector_id}/api-keys/{key_id} | Revoke a key. |
These management endpoints authenticate as your dashboard user, not with the event key itself.
Send an event
curl -X POST https://app.plugsync.com/api/v1/events \
-H "x-api-key: ps_live_..." \
-H "content-type: application/json" \
-d '{"event": "order_created", "data": {"order_id": "A-1001", "total": 129.9}}'Request body
| Field | Type | Required | Notes |
|---|---|---|---|
event | string | yes | The event type. Flows match on (source: "event_api", event_type); wildcards like order.* are declared on the flow, not here. |
data | object | yes* | The business payload. Inside flow manifests this exact object is addressed as $.payload.*. |
timestamp | ISO 8601 datetime | no | The business time the event occurred (not the send time). Used as occurred_at by most_recent conflict arbitration when no source_modified_path resolves. Naive datetimes are treated as UTC. Absent, the ingest instant is used. |
storeId | string | no | Multi-brand routing. Can also be sent as the x-store-id header; falls back to the connector’s default store. |
entities | array | no* | Legacy multi-entity shape (max 200): each entry enqueues one {entity.type}_synced event. Prefer event + data for new integrations. |
*Send either data (canonical) or entities (legacy). A stray payload key
is rejected with a 422 — the business payload goes under data; the
$.payload.* spelling exists only inside flow manifests.
Response
202 Accepted — the event is queued, not yet processed:
{
"event_id": "8b1f...",
"status": "processing",
"entities_count": 1,
"hint": null
}hint, when present, is an advisory string worth logging. It fires in two
cases:
- No subscribed flow: no published flow listens on
(event_api, <your event_type>). The event is accepted but nothing will sync until a matching flow is published. This is computed against the published config, so a draft-only flow still triggers the hint. - Payload too large for the real-time path: a legacy
entitiespayload above 50 entries gets pointed at bulk import.
Errors
| Status | Meaning | What to do |
|---|---|---|
401 | Missing, malformed, or revoked API key. | Check the x-api-key header; mint a new key if revoked. |
422 | Body validation failed (e.g. a stray payload key, missing event). | The detail message states the exact field. |
423 | The connector is paused or in error: ingestion is blocked. | Resume the connector from the dashboard. |
429 | Monthly event quota exceeded for the org’s tier. | The detail carries events_used_this_period and period_resets_at. |
503 | Transient infrastructure failure (ingest queue or attachment store unavailable). | Retry with backoff; nothing was enqueued. |
These are the errors of the ingest call. Once an event is accepted with a
202, what happens to it if a flow step fails (how many times it is retried,
which terminal state it lands in, how to replay it) is covered in
Reliability: retry, failures, replay.
Check processing status
curl https://app.plugsync.com/api/v1/events/{event_id}/status \
-H "x-api-key: ps_live_..."Returns per-entity progress (entities_total / entities_done /
entities_failed / entities_skipped) plus a row per entity with action,
status, hubspot_id, and error detail. The overall status:
| Status | Meaning |
|---|---|
processing | Not all entities have reached a terminal state yet. |
delayed | The event has been queued longer than the stuck deadline (PLUGSYNC_EVENT_STATUS_STUCK_DEADLINE_SECONDS, default 600s) but no entity has been processed at all. This is a slow FIFO backlog (queue depth, other-tenant contention), not a failure: keep polling. The hint field explains how long the event has been queued. |
completed | Every entity synced successfully. |
skipped | Every entity was a no-op: no published flow matched. Nothing was written. |
partial_failure | At least one entity failed, or only part of the event synced (the rest skipped), or the event is past the stuck deadline with at least one entity already reported. |
delayed and partial_failure both fire off the same stuck-deadline check, but
they mean different things: delayed means the event has not been picked up by
a worker at all yet (nothing to worry about beyond queue latency); partial_failure
past the deadline means at least one entity did get processed but the rest never
reported (worth investigating). Do not treat delayed as a failure state in your
integration’s error handling: it is transient, exactly like processing.
Verify a processed event
curl https://app.plugsync.com/api/v1/events/{event_id}/verify \
-H "x-api-key: ps_live_..."Reads the current HubSpot record back and diffs it against what the event’s
own published flow projects for its payload: one entry per entity with
status of ok / diff / absent / error / not_synced /
not_verifiable and per-property differences. Call /status first — this
endpoint does not wait for processing to finish.
not_verifiable covers form-only flows (a single submit_form step): the
submission was accepted, but HubSpot resolves the contact itself (no record
id and no identity mapping), so there is no CRM record to read back and
compare.
Bulk import
POST /api/v1/events/bulk takes up to 5000 entities per request (10 MiB
combined) and processes them as a chunked job with a terminal, job-level
status — built for day-0 loads and migrations, not the per-event real-time
path:
{
"entity_type": "contact",
"external_id_property": "external_id",
"idempotency_key": "initial-load-2026-07",
"entities": [ { "external_id": "C-1", "email": "a@example.com" }, ... ]
}- Idempotency: replaying the same
idempotency_keywith an identical body returns the original job; a reused key with a different entity count is rejected with409 body_mismatch. - Progress:
GET /api/v1/events/bulk/{job_id}/status(job counters) andGET /api/v1/events/bulk/{job_id}/rows(per-row ledger, filterable). - Row replay:
POST /api/v1/events/bulk/{job_id}/rows/{reference}/replayreprocesses one failed row through the same pipeline and reconciles the outcome into the same ledger entry. - Quota: gated upfront on the org’s monthly event quota (
429with the same detail shape as the real-time path); rate limits are surfaced in theX-RateLimit-*response headers. - Larger migrations (e.g. ~40k rows) split into consecutive requests of up to 5000 rows each.
Related reference
- Reliability: retry, failures, replay - the retry policy with its real numbers, the terminal event states, and the replay endpoints.
- Flow authoring — make a flow actually subscribe to your event type.
- Event sources —
event_apiis one source type among webhook, HubSpot journal, poller, scheduled, file. - Conflicts — how
timestampfeedsmost_recentarbitration on bidirectional connectors. - Getting started, step 8 — the same call in tutorial form.