API reference
The Plugsync Event Ingestion API lets you push events from any system to a connector. Ingestion endpoints require a connector API key sent in the x-api-key header.
Base URL
https://app.plugsync.com
Authentication
Every request must include an x-api-key: <connector_api_key> header. API keys are scoped to a connector (ps_live_...). You can find or rotate them under Connectors → [your connector] → API keys.
curl -H "x-api-key: ps_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
https://app.plugsync.com/api/v1/events/<event_id>/status
POST /api/v1/events
Ingest a single event.
Request body:
{
"event": "contact_upserted",
"data": {
"external_id": "user-42",
"email": "[email protected]",
"first_name": "Alice"
}
}
The business payload goes under data, and any identifier your system uses (such as external_id) lives inside it, not at the top level.
| Field | Type | Required | Notes |
|---|---|---|---|
event |
string | yes | Event type; event flows subscribe to it. |
data |
object | yes | The business payload; addressed as $.payload.* inside flows. |
storeId |
string | no | Required for multi-store connectors. Identifies the source store. |
timestamp |
string | no | ISO-8601 timestamp. Defaults to server time. |
Response:
{
"event_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "processing"
}
The response may also carry an advisory hint string — for example when no published event flow subscribes to the event you sent. Such an event is still accepted (202) but nothing is synced: polling GET /api/v1/events/<event_id>/status reports it as skipped, never completed.
Webhook signature verification
Event sources of type webhook receive events at POST /api/webhooks/<connector_id>/<source_name>. This endpoint takes no API key: the HMAC signature is the authentication. The event source’s config.inbound declares the scheme:
{
"inbound": {
"verify_signature": "hmac_sha256",
"signature_header": "x-signature-sha256",
"encoding": "base64",
"event_type_path": "$.event_type"
}
}
verify_signature is the algorithm name (hmac_sha256 or stripe_signing_v1), not a boolean. signature_header, encoding (base64 default, hex opt-in) and event_type_path are optional, with the defaults shown above.
For hmac_sha256, sign the raw request body with your webhook secret (no timestamp is involved) and send the result in the configured header:
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)
curl -X POST https://app.plugsync.com/api/webhooks/<connector_id>/<source_name> \
-H "Content-Type: application/json" \
-H "x-signature-sha256: $SIG" \
-d "$BODY"
Equivalent Python:
import base64, hashlib, hmac
signature = base64.b64encode(
hmac.new(secret.encode(), body_bytes, hashlib.sha256).digest()
).decode()
A misconfigured source answers 400 with error code webhook_source_misconfigured; a wrong or missing signature answers 401.
Rate limits
Two independent limits apply to the Event Ingestion API, both scoped to your organization:
- Per-second rate limit. A sliding one-second window caps requests based on your organization’s tier: 10 requests/second on the entry tier, 50 on Pro, 100 on Enterprise. Every response reports the cap applied to your org in the
X-RateLimit-Limitheader, along withX-RateLimit-RemainingandX-RateLimit-Reset. Exceeding it returns429 Too Many Requests(plain-stringdetail) with aRetry-Afterheader in seconds. - Monthly event quota. 2,000 events/month on the Developer tier, 25,000 on Starter, 75,000 on Pro, 400,000 on Scale; Enterprise plans include a custom fair-use band. Exceeding it returns a structured
429 quota_exceeded(see the error table below).
Error responses
Every error response carries a detail field, but its shape depends on the error:
- Simple failures use a plain string, for example
{ "detail": "Invalid or revoked API key" }. - Machine-actionable failures use an object with a stable
errorcode you can branch on, plus whatever extra context applies to that case. Not all of them include amessage:quota_exceededdoesn’t, useupgrade_urlandperiod_resets_atinstead. - Validation errors (422) are a third shape:
detailis a list of field-level errors, not a string or an object. - The 500 fallback breaks the
detail-only pattern: the body is{ "detail": "internal_error", "request_id": "<uuid>" }, withrequest_idas a sibling key rather than nested insidedetail.
| Status | error | When |
|---|---|---|
| 401 | (plain string) | Missing or invalid x-api-key header, e.g. "Missing x-api-key header" or "Invalid or revoked API key". |
| 403 | bulk_import_not_enabled |
Bulk endpoint used but bulk import isn’t enabled for this connector; contact support to enable it. |
| 409 | body_mismatch (plain string) |
Bulk replay: same idempotency_key as an existing job, but a different number of entities. |
| 422 | (validation error) | Body fails schema validation. FastAPI returns field-level detail (a list of {loc, msg, type}). |
| 423 | (plain string) | Connector status blocks ingestion, e.g. "Connector is paused, ingestion is blocked". |
| 429 | quota_exceeded |
Monthly event quota exceeded for the org’s tier. Structured, no message key. |
| 429 | (plain string) | Per-second rate limit hit for the org, e.g. "Rate limit exceeded for org". Separate from the quota above. |
| 500 | internal_error |
Unhandled server error. Safe to retry with backoff. |
| 503 | event_ingestion_unavailable |
Enqueueing the event failed (e.g. the queue was unreachable). Safe to retry. |
Every response, success or error, carries an X-Request-ID header: include it when contacting support.
Idempotency
Single events sent to POST /api/v1/events are not deduped: sending the same event/data twice creates two events. For bulk re-syncs, use POST /api/v1/events/bulk with an idempotency_key: replaying the same key returns the original job instead of creating a new one, as long as the replay has the same number of entities as the original request. If the entity count differs, the endpoint returns 409 body_mismatch rather than silently accepting the new payload. The bulk endpoint requires bulk import enabled on the connector (contact support), otherwise it returns 403 bulk_import_not_enabled.