Skip to content

API (Beta)

API v1 reference

OutboundSync API v1 reference — endpoints, sources and destinations vocabulary, webhooks (Sync Monitoring), events, blockers, and security.

When to use this guide: This is the complete, authoritative contract for OutboundSync API v1 — the vocabulary, every endpoint’s request and response shape, webhooks (Sync Monitoring), the account-status blocker and warning codes, and the security model. To create and manage keys first, see Creating API keys. To send a live request from the browser, see Try the API. How-to for Webhooks: OutboundSync webhooks.

Public REST API for OutboundSync account introspection, pipeline status checks, and platform-emitted event webhooks. Use at any lifecycle stage — before a campaign, mid-flight when CRM data looks wrong, or after completion. Account endpoints use a Bearer token; discovery is auth-free:

Authorization: Bearer osapi_<key>

Authenticated account endpoints under /api/v1 use a Bearer token. Discovery routes are auth-free — see Discovery (auth-free). The base URL is:

https://app.outboundsync.com/api/v1

Introspection endpoints (/me, /account/status, /connections, /sources, /contacts/outreach, /destinations, /destinations/reply-relays) are GET. Observability list endpoints (/requests, /syncs, /deliveries) are also GET and require a from/to date range — see Pipeline observability. Pipeline observability also includes POST /api/v1/syncs/:id/retry and POST /api/v1/destinations/:id/deliveries/:deliveryId/replay (both require write), plus GET /api/v1/account/metrics. Webhook management also includes POST, PATCH, and DELETE (see Webhooks). Conceptual how-to: OutboundSync webhooks. Here is a typical introspection call in JavaScript with error handling:

const response = await fetch('https://app.outboundsync.com/api/v1/me', {
headers: {
Authorization: `Bearer ${process.env.OUTBOUNDSYNC_API_KEY}`,
},
});
if (!response.ok) {
throw new Error(`OutboundSync API request failed: ${response.status}`);
}
const data = await response.json();

Each authenticated endpoint below shows a copyable request sample and an example response. Signing secrets appear only once on webhook create/rotate (see Security). Every error response is JSON ({ statusCode, message, error }). For 401 / 403 / 404 / 429 / 504 handling and a retry pattern, see Errors and rate limits.

The word “webhook” is overloaded in OutboundSync. The public API separates sources → destinations from webhooks (Sync Monitoring):

Concept Schema today UI label today API term Path
Inbound URLs sequencers POST events to (POST /webhooks/:code) Webhook model “Sources” source GET /api/v1/sources
One inbound POST accepted at a source SmartleadPayload — request GET /api/v1/requests
One CRM write attempt for a received request WebhookLog + WebhookLogResult “Sync history” sync GET /api/v1/syncs
Customer URLs OutboundSync forwards received events to UserAccount + UserAccountWebhook junction “Forwarding destinations” destination (forwarding) GET /api/v1/destinations
One outbound POST attempt to a destination UserAccountWebhookLogs — delivery GET /api/v1/destinations/:id/deliveries
Reply relay (Reply-CC to a sales rep) ReplyRelay + WebhookSettings.replyRelayId “Reply relays” destination (reply relay) GET /api/v1/destinations/reply-relays
OutboundSync platform-emitted events (Sync Monitoring) WebhookEndpoint + PlatformEvent + PlatformEventDelivery “Webhooks” webhooks /api/v1/webhooks, /api/v1/events

Notes:

  • Webhooks are OutboundSync’s own events (Stripe/HubSpot/Segment convention), delivered to customer-registered HTTPS endpoints — not inbound sources, destination forwarding/reply relays, inbound requests, or CRM syncs. /events is that platform log; inbound receipts live under /requests and CRM write attempts under /syncs.
  • CRM sync attempts appear in the admin UI under History → Sync history.
  • “Destination” is the umbrella term for managed integrations and raw webhook URLs. Connection capability capabilities.destinations (DB: canUserAccount) gates both forwarding and reply relays. CRM writes are syncs, not destinations.
  • Delivery is exposed top-level (/deliveries) and nested (/destinations/:id/deliveries). It is parent-disambiguated from Sync Monitoring /webhooks/.../deliveries (platform event attempts use different ids and status enums).
  • eventTypes[] lives on the forwarding destination binding, not on the source. Bindings also still appear nested on GET /sources (sources[].destinations[]).
  • The physical inbound route /webhooks/:code is unchanged — it is the value of a source’s url field. Legacy sources created before code-based routing use /webhooks/:hubId/:ownerId; the url field always reflects the correct form.

Now is either what is live in production, or reserved (namespace held — not a callable resource yet). Destination bindings still appear as fields on sources; the forwarding catalog is GET /api/v1/destinations. /events remains the platform webhook log — not inbound receipts.

Namespace Now Later
/api/v1/account/* status, metrics usage, limits; /me stays as key-identity check
/api/v1/connections list w/ embedded status /:id, /:id/test, /:id/properties
/api/v1/contacts outreach (prior-outreach summary by email and/or social profile URL) timeline, batch
/api/v1/accounts reserved outreach (prior-engagement rollup by company domain)
/api/v1/sources list w/ config + destinations summary /:id, /:id/logs, POST create
/api/v1/requests inbound receipts (cursor + date range) —
/api/v1/syncs CRM sync attempts (cursor + date range + retry) —
/api/v1/destinations list + get + deliveries + replay create/update
/api/v1/destinations/reply-relays list reply relay catalog create/update/bind
/api/v1/deliveries account-wide delivery export (destinationId filter) —
/api/v1/blocklists list; /:id/pause; /:id/resync GET /:id/entries; DELETE /:id/entries (clear local entries — not shipped)
/api/v1/webhooks CRUD, rotate-secret, test, deliveries, replay additional event types
/api/v1/events queryable event log subscription filtering refinements

Rules: plural kebab-case resources; status embedded on resources; cross-cutting checks under account/; every list response wrapped in a named key ({ "sources": [...] }) except cursor-paginated observability lists, which use { "data", "hasMore", "nextCursor" }. GET /api/v1/me is the identity exception — flat top-level fields plus a links object for related endpoints.

Endpoints below have a Try it link that opens them in the interactive console, preselected — paste a key and send. Discovery routes are fetch-the-URL only (they are not in the console menu).

Method Path Description
GET /api/v1/openapi.json OpenAPI 3.1 document (JSON)
GET /api/v1/openapi.yaml OpenAPI 3.1 document (YAML; source form)

Stable production URLs:

  • https://app.outboundsync.com/api/v1/openapi.json
  • https://app.outboundsync.com/api/v1/openapi.yaml

These routes do not require an API key. Responses are cacheable (Cache-Control: public, max-age=300). The document is hand-maintained OpenAPI 3.1 (not generated from Nest decorators). It covers discovery, platform health, and the authenticated endpoints on this page (account introspection, requests, syncs, destinations, deliveries, blocklists, webhooks, and events).

Self-discovery: every /api/v1/* response on app.outboundsync.com — including a pre-auth 401 — carries an RFC 8631 Link header advertising the spec, so a client can find it from any response without knowing the URL up front:

Link: </api/v1/openapi.json>; rel="service-desc", <https://outboundsync.com/docs/api/v1>; rel="service-doc"
Try it in the console

Returns account, API key metadata, accessible CRM connections, and links to related v1 endpoints. Use as the bootstrap call after authentication.

curl 'https://app.outboundsync.com/api/v1/me' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

apiKey.connectionId is set when the key is connection-scoped (connectionScope: "connection"); it is null for account-scoped keys and matches the single entry in connections[].

connections[] order matches accessibleConnectionIds — the same order as /connections and /account/status.

Response: MeResponse

{
"account": {
"id": 42,
"email": "harris@outboundsync.com"
},
"apiKey": {
"name": "prod",
"scopes": ["read"],
"connectionScope": "account",
"connectionId": null
},
"connections": [
{
"id": 7,
"crm": "HUBSPOT",
"organizationId": "123",
"organizationDomain": "acme.com"
}
],
"links": {
"accountStatus": {
"href": "/api/v1/account/status",
"description": "Pipeline readiness with blockers and warnings for each connection."
},
"accountMetrics": {
"href": "/api/v1/account/metrics",
"description": "Request, sync, and destination-delivery counts for a date range."
},
"connections": {
"href": "/api/v1/connections",
"description": "CRM connections with OAuth status and plan capabilities."
},
"destinations": {
"href": "/api/v1/destinations",
"description": "Forward URLs with delivery history for warehouse sync and replay."
},
"requests": {
"href": "/api/v1/requests",
"description": "Inbound receipts accepted at sources."
},
"destinationsReplyRelays": {
"href": "/api/v1/destinations/reply-relays",
"description": "Reply relay destinations (CC a sales rep on prospect replies)."
},
"sources": {
"href": "/api/v1/sources",
"description": "Inbound paste URLs, platform config, and destination bindings."
},
"contactsOutreach": {
"href": "/api/v1/contacts/outreach",
"description": "Prior outreach summary for a contact email and/or social profile URL."
},
"syncs": {
"href": "/api/v1/syncs",
"description": "CRM sync attempts with status filters and retry."
},
"documentation": {
"href": "https://outboundsync.com/docs/api/v1/",
"description": "OutboundSync API v1 reference."
},
"openapi": {
"href": "/api/v1/openapi.json",
"description": "OpenAPI 3.1 machine-readable contract for API v1."
}
}
}

For connection-scoped keys, connectionScope is "connection", connectionId matches connections[0].id, and connections[] has one element.

links also advertises related collections (requests, syncs, destinations, accountMetrics, contactsOutreach). This page documents requests, syncs, destinations (including deliveries and replay), account metrics, and prior outreach. Observability list and metrics endpoints share the same date-range and cursor conventions.

Try it in the console

Point-in-time pipeline status across all connections the key can access. Answers: can OutboundSync receive events and sync them to the CRM right now? Connection-scoped keys receive a one-element array.

This is account-specific and requires a key. For platform-wide availability (independent of your account), use the public, unauthenticated Platform health endpoints instead.

curl 'https://app.outboundsync.com/api/v1/account/status' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: AccountStatusResponse

The response has three outcome layers:

Layer Field Meaning
Red light blockers[] Sync cannot work until resolved. Gates ready.
Yellow light warnings[] Optional features not configured or non-fatal issues. Never gates ready.
Detail connections[].* per-component status Embedded component state for each connection.

Warning remediations in the sample below use Sources-vocabulary deep links (Forwarding destinations + Reply relays).

{
"ready": true,
"checkedAt": "2026-07-03T12:00:00.000Z",
"connections": [
{
"connectionId": 7,
"crm": "HUBSPOT",
"ready": true,
"crmConnection": {
"status": "ready",
"crm": "HUBSPOT",
"organizationId": "123",
"organizationDomain": "acme.com"
},
"sources": {
"status": "ready",
"count": 2,
"platforms": ["smartlead", "instantly"]
},
"destinations": {
"status": "not_configured",
"count": 0,
"eventTypes": [],
"forwardingCount": 0,
"replyRelayCount": 0
},
"blocklists": {
"status": "not_configured",
"enabledCount": 0,
"lastError": null,
"lastFetchedAt": null
}
}
],
"blockers": [],
"warnings": [
{
"code": "destinations_not_configured",
"connectionId": 7,
"crm": "HUBSPOT",
"message": "No destinations are configured — events still sync to HubSpot; nothing is forwarded to external URLs or sent as a reply relay.",
"remediation": "Optional: add forwarding URLs under Forwarding destinations (https://app.outboundsync.com/admin/dashboard/services) or a reply relay (https://app.outboundsync.com/admin/dashboard/reply-relays).",
"docUrl": "https://outboundsync.com/docs/api/v1/#warning-destinations_not_configured"
},
{
"code": "blocklists_not_configured",
"connectionId": 7,
"crm": "HUBSPOT",
"message": "No block list is configured — sync to HubSpot is active but no contacts are being suppressed.",
"remediation": "Optional: configure a block list in the OutboundSync dashboard (https://app.outboundsync.com/admin/dashboard/smartlead-block-lists) if you need to skip certain contacts.",
"docUrl": "https://outboundsync.com/docs/api/v1/#warning-blocklists_not_configured"
}
]
}

destinations.count is the sum of forwardingCount (distinct forwarding URLs) + replyRelayCount (bound reply relays). The two breakdown fields are additive: clients that previously read destinations.count as “forwarding URLs” should switch to forwardingCount.

When CRM is disconnected, ready is false and blockers contains crm_disconnected — warnings may also be present but do not affect ready.

HTTP semantics: authenticated-but-not-ready returns HTTP 200 with ready: false. Auth failures return 401/403; rate limits return 429.

Try it in the console

CRM connections with embedded status and capability flags.

curl 'https://app.outboundsync.com/api/v1/connections' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: ConnectionsResponse

{
"connections": [
{
"id": 7,
"crm": "HUBSPOT",
"status": "ready",
"organizationId": "123",
"organizationDomain": "acme.com",
"capabilities": {
"sync": true,
"destinations": true,
"blocklists": true
},
"createdAt": "2025-01-01T00:00:00.000Z"
}
]
}
Try it in the console

Inbound sources with the URL to paste into a sequencer, platform, config summary, and destination bindings.

curl 'https://app.outboundsync.com/api/v1/sources' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: SourcesResponse

{
"sources": [
{
"id": 10,
"url": "https://sync.outboundsync.com/webhooks/abc123",
"platform": "smartlead",
"connectionId": 7,
"crm": "HUBSPOT",
"config": {
"createOrUpdateCompany": true,
"createOrUpdateTask": true,
"assignContactOwner": true,
"salesforceObjectType": null
},
"destinations": [
{
"url": "https://hooks.example.com/os",
"description": "Clay",
"eventTypes": ["EMAIL_SENT", "EMAIL_REPLY"],
"isDelayed": false
}
],
"replyRelay": {
"id": 1,
"description": "AE reply relay — Acme",
"ccEmail": "rep@acme.com",
"delayMinutes": 1
},
"createdAt": "2025-01-02T00:00:00.000Z"
}
]
}
Try it in the console

Prior outreach summary for a contact across every CRM connection the API key can access. One call per contact from an agent, low-code workflow, or enrichment column (ZoomInfo, Clay, Databar, Freckle, and similar). Setup: Check prior outreach for a contact.

Query (at least one required):

Param Required Description
email if no profileUrl Contact email (normalized to lowercase).
profileUrl if no email LinkedIn (/in/… or /pub/…) or X/Twitter profile URL. Scheme optional. Repeat the param or comma-separate (max 5).
GET /api/v1/contacts/outreach?email=jane@acme.com
GET /api/v1/contacts/outreach?profileUrl=https://www.linkedin.com/in/jane-doe
GET /api/v1/contacts/outreach?email=jane@acme.com&profileUrl=https://linkedin.com/in/jane-doe&profileUrl=https://x.com/jane
curl 'https://app.outboundsync.com/api/v1/contacts/outreach?email=jane@acme.com' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: ContactOutreachResponse

{
"email": "jane@acme.com",
"query": {
"email": "jane@acme.com",
"profileUrls": ["https://linkedin.com/in/jane-doe", "https://x.com/jane"]
},
"found": true,
"summary": {
"everContacted": true,
"everEmailed": true,
"everOpened": true,
"everCalled": false,
"everSocialTouched": true,
"totalEvents": 47,
"firstTouchAt": "2025-11-02T14:01:00.000Z",
"lastTouchAt": "2026-07-20T18:22:00.000Z",
"daysSinceLastTouch": 9,
"lastEventType": "EMAIL_REPLY",
"lastPlatform": "smartlead"
},
"eventTypes": [
{ "eventType": "EMAIL_SENT", "count": 12, "firstAt": "2025-11-02T14:01:00.000Z", "lastAt": "2026-07-18T10:00:00.000Z" },
{ "eventType": "EMAIL_OPEN", "count": 28, "firstAt": "2025-11-02T15:00:00.000Z", "lastAt": "2026-07-19T12:00:00.000Z" },
{ "eventType": "EMAIL_REPLY", "count": 1, "firstAt": "2026-07-20T18:22:00.000Z", "lastAt": "2026-07-20T18:22:00.000Z" }
],
"platforms": [
{ "platform": "smartlead", "count": 40, "lastAt": "2026-07-20T18:22:00.000Z", "lastEventType": "EMAIL_REPLY" },
{ "platform": "heyreach", "count": 7, "lastAt": "2026-06-01T09:00:00.000Z", "lastEventType": "MESSAGE_SENT" }
],
"outcomes": {
"replied": true,
"lastReplyAt": "2026-07-20T18:22:00.000Z",
"bounced": false,
"unsubscribed": false,
"lastCategoryName": "Interested",
"lastCategoryAt": "2026-07-21T09:00:00.000Z"
},
"doNotContact": {
"value": false,
"reasons": []
},
"blocklists": {
"evaluated": false,
"matched": false,
"matches": []
}
}

Notes:

  • All supplied identities (email + every profileUrl) are OR-unioned into one aggregate. Pass identities for a single contact row only — mixing unrelated people merges their engagement.
  • Lookups use an indexed, case-insensitive match on webhook_logs.to_email (lower(to_email), backed by a functional index) against the email and expanded profile URL variants (http/https, www, x.com↔twitter.com, trailing slash; query/hash stripped). LinkedIn overlay/mwlite/country hosts and X status URLs are canonicalized to /in|pub/{slug} and /{handle}.
  • Social-only ledger coverage: HubSpot and Salesforce backfill to_email with the profile URL when the lead has no email, so profileUrl lookups resolve for those connections. Attio, Close, HighLevel, and Pipedrive leave to_email empty for social-only events, so a profileUrl lookup will not match them today — pass email when you have it.
  • Category signal (outcomes.lastCategory*) matches lead_email / lead_identity; those rows are usually email-keyed. Prefer email for category outcomes.
  • Counts are deduped by inbound payload id per event type × platform (CRM retry duplicates).
  • everContacted is true for send / reply / call / outbound social message — not open/click-only or passive views/likes.
  • found is true when webhook log rows and/or a lead category signal exist for the identities.
  • summary.daysSinceLastTouch is whole days since lastTouchAt (floor), or null when there is no last touch — useful for cadence filters without date math. Category-only matches set found: true with daysSinceLastTouch: null (still eligible under a “null or ≥ N days” cadence filter).
  • doNotContact reports hard signals only: bounced, unsubscribed.
  • blocklists is a reserved CRM blocklist namespace. In this release evaluated is always false, matched is always false, and matches is always [] — do not read that as “not on a blocklist.” Reverse lookup ships when an indexed match path is available.
  • Domain / company rollup is a separate reserved path — GET /api/v1/accounts/outreach?domain= (not implemented). Do not pass a bare domain as email or profileUrl on this contact endpoint.
  • Timestamps are processing time (webhook_logs.created_at), not the sequencer event timestamp.
  • Events held behind category-based filters may lack log rows until release; a category signal alone still sets found: true.
  • Prefer query.email for field mapping. Top-level email is a deprecated alias of query.email (kept for spreadsheet connector mapping).
  • Rate limit: 600 requests / 60s per account — a separate bucket from the general v1 120/min account limit. See Errors and rate limits.

Neither identity (no email and no profileUrl), an invalid email/profileUrl, or more than 5 profileUrl values returns 400.

When no prior outreach exists: found: false, empty arrays, booleans false, daysSinceLastTouch: null.

Workflow setup: Check prior outreach for a contact (agent, low-code HTTP module, ZoomInfo, Clay, Databar, Freckle, or any Bearer GET).

Try it in the console

Catalog of forwarding destinations (UserAccount rows) for accessible connections — customer URLs OutboundSync forwards received events to. Distinct from reply relays. Bindings also still appear nested on GET /api/v1/sources.

No query parameters. The list is not cursor-paginated. Rows are ordered by id ascending. When the key has no accessible connections, the response is { "destinations": [] }.

curl 'https://app.outboundsync.com/api/v1/destinations' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: DestinationsResponse

{
"destinations": [
{
"id": 3,
"url": "https://hooks.example.com/os",
"description": "Clay",
"connectionId": 7,
"eventTypes": ["EMAIL_SENT", "EMAIL_REPLY"],
"isDelayed": false,
"sourceIds": [10],
"createdAt": "2025-01-03T00:00:00.000Z"
}
]
}

eventTypes is the union of event types across bindings. isDelayed is true if any binding is delayed. sourceIds are the unique source ids bound to this destination.

Try it in the console

One forwarding destination by id. Returns 404 (Destination 3 not found) when that destination is missing or its connection is not in the key’s access.

curl 'https://app.outboundsync.com/api/v1/destinations/3' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: DestinationResource — same object shape as one element of GET /api/v1/destinations destinations[].

Try it in the console

Lists reply relay destinations for accessible connections (the catalog under Connected accounts → Reply relays). Creating and binding reply relays requires Destinations (capabilities.destinations / canUserAccount); this list endpoint returns catalog rows for connections the key can access.

curl 'https://app.outboundsync.com/api/v1/destinations/reply-relays' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: DestinationsReplyRelaysResponse

{
"replyRelays": [
{
"id": 1,
"connectionId": 7,
"description": "AE reply relay — Acme",
"ccEmail": "rep@acme.com",
"delayMinutes": 1,
"bodyTemplates": [
"Thanks for the reply — I'm transferring this conversation to my other inbox (CC'd).\n\nPlease continue the thread with me there.\n"
],
"categoryAllowlist": [{ "id": "interested", "name": "Interested" }],
"sourceIds": [10],
"createdAt": "2025-01-04T00:00:00.000Z"
}
]
}

categoryAllowlist is an array of { id, name } objects (max 50). An empty array means all replies; when non-empty, Reply-CC checks the lead’s current Smartlead categories or EmailBison tags at execute time and continues only on a match.

CRM→SEP blocklist sync configurations for connections the API key can access. Distinct from the contacts/outreach blocklists.* fields, which stay unevaluated in this release.

SEP push is additive. pause and resync change OutboundSync’s pipeline only — they do not clear the sales engagement platform blocklist.

There is no public “resume incremental” verb. Re-enabling after pause is always a full resync. The dashboard’s Pause blocking / Resync blocking row actions use a separate cookie-authenticated route. Resync blocking is the same full resync as POST …/resync, not a cheap continue from the last cursor. API clients use pause / resync.

Day-to-day UI workflow: Block list maintenance guide.

Try it in the console

Named list of CRM→SEP blocklist syncs for accessible connections. This is not a date-range or cursor page.

Query parameters:

Name Required Description
connectionId no Limit results to one accessible connection id. An id outside the key’s access returns { "blocklists": [] } — not 404.

A key with zero API-enabled connections never reaches this route — auth returns 403 (API access is not enabled for this account), same as every other authenticated /api/v1 call.

curl 'https://app.outboundsync.com/api/v1/blocklists' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: BlocklistsResponse

{
"blocklists": [
{
"id": 12,
"connectionId": 7,
"listName": "Do not contact",
"crm": "HUBSPOT",
"platform": "smartlead",
"blockType": "ADDRESS",
"status": "SYNCING_NEW_CONTACTS",
"isEnabled": true,
"clientId": null,
"syncedCount": 1840,
"pendingCount": 12,
"lastFetchedAt": "2026-08-20T14:02:11.000Z",
"lastError": null,
"lastAttemptedAt": "2026-08-20T14:02:11.000Z",
"createdAt": "2026-07-01T09:00:00.000Z"
}
]
}

blockType is ADDRESS or DOMAIN. status is CREATED, FETCHING, SYNCING, SYNCED, or SYNCING_NEW_CONTACTS. crm is the connection profile when known (for example HUBSPOT or SALESFORCE), otherwise null.

Try it in the console

Stop fetch and SEP push (isEnabled: false). No request body. Requires the write scope. Connection-scoped keys can pause (unlike /webhooks*). The write check runs before the list is loaded, so a read-only key receives 403 even for an unknown id.

If status was FETCHING or SYNCING, the persisted status becomes SYNCING_NEW_CONTACTS so a later resync can reclaim. In-flight fetch jobs that already passed the enabled check may still finish writing local entities; those rows will not push while paused. Resync while that job is still active returns 409 (Blocklist resync already in progress) — skip or back off. Response status is the persisted post-pause value. Already-paused lists return 200 again — they do not 409.

curl 'https://app.outboundsync.com/api/v1/blocklists/12/pause' \
  -X POST \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: BlocklistPauseResponse

{
"id": 12,
"isEnabled": false,
"status": "SYNCING_NEW_CONTACTS"
}
Status Meaning
200 Paused. isEnabled is false.
403 Missing write scope. Message: API key requires the “write” scope.
404 Blocklist missing or not in the key’s access (Blocklist 12 not found).
Try it in the console

Enable the list, clear lastFetchedAt / errors, and queue a full CRM reload. No request body. Requires the write scope. Connection-scoped keys can resync. The write check runs before the list is loaded, so a read-only key receives 403 even for an unknown id.

Local wipe happens in the fetch job. SEP push stays additive — resync does not clear the sequencer. Enqueue only — the response does not wait for the CRM fetch to finish.

curl 'https://app.outboundsync.com/api/v1/blocklists/12/resync' \
  -X POST \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: BlocklistResyncResponse

{
"id": 12,
"queued": true,
"isEnabled": true,
"status": "FETCHING"
}
Status Meaning
200 Queued. status is FETCHING.
403 Missing write scope, or block lists not enabled on the connection (Block lists are not enabled for this connection).
404 Blocklist missing or not in the key’s access (Blocklist 12 not found).
409 Already FETCHING/SYNCING, or a fetch job is still active (Blocklist resync already in progress); or no CRM source (Blocklist has no CRM source to resync from).

With a write key:

  1. GET /api/v1/blocklists — pick the id.
  2. Optionally clear the matching block list in the sequencer (OutboundSync will not do this).
  3. POST /api/v1/blocklists/:id/resync.
  4. On 409, skip or back off.
  5. Poll GET /api/v1/blocklists until status leaves FETCHING/SYNCING, or inspect lastError.

Date-filtered, cursor-paginated inbound receipts, CRM sync attempts, and destination delivery attempts for warehouse sync and operational dashboards. Named requests (not events) because /events is reserved for the platform webhook log. A sync is one CRM write attempt for a received request. A delivery is one outbound POST attempt to a forwarding destination.

All list and metrics endpoints in this section require from and to (ISO-8601), except GET /api/v1/syncs/:id, POST /api/v1/syncs/:id/retry, and POST /api/v1/destinations/:id/deliveries/:deliveryId/replay. The span must not exceed 31 days, and from must be less than or equal to to. Default limit is 100 (max 500). Cursor responses use { "data", "hasMore", "nextCursor" } and are ordered newest-first by (createdAt, id). The cursor is opaque — pass nextCursor back as cursor to fetch the next page.

Missing or invalid from/to, a range longer than 31 days, or a malformed cursor returns 400. Connection-scoped keys only see receipts, syncs, and deliveries for that connection.

Try it in the console

Inbound webhook receipts accepted at a source’s ingress URL — one row per accepted POST.

Query parameters:

Name Required Description
from yes Start of the range (ISO-8601).
to yes End of the range (ISO-8601).
sourceId no Restrict to one source. Unknown or inaccessible ids return 404.
cursor no Opaque cursor from a previous page.
limit no Page size (default 100, max 500).

When the key has no accessible sources (and sourceId is omitted), the response is an empty page — not 404.

curl 'https://app.outboundsync.com/api/v1/requests?from=2026-08-01T00:00:00.000Z&to=2026-08-13T23:59:59.999Z' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: RequestsResponse

{
"data": [
{
"id": 9001,
"sourceId": 10,
"connectionId": 7,
"path": "abc123",
"createdAt": "2026-08-12T15:04:05.000Z"
}
],
"hasMore": false,
"nextCursor": null
}

path is the source ingress code (the slug in the paste URL), not the full URL. sourceId / connectionId are null only if the backing source row is missing.

Try it in the console

Count of inbound receipts in the date range, optionally filtered by sourceId. cursor and limit are ignored.

Query parameters: from and to (required), sourceId (optional). Same 31-day rule and 404 behavior as the list endpoint.

curl 'https://app.outboundsync.com/api/v1/requests/metrics?from=2026-08-01T00:00:00.000Z&to=2026-08-13T23:59:59.999Z' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: RequestMetrics

{
"count": 128
}
Try it in the console

Same list as GET /api/v1/requests, with sourceId taken from the path instead of the query string. Returns 404 when that source is not in the key’s accessible connections.

Query parameters: from and to (required), cursor and limit (optional). Do not also pass sourceId as a query parameter.

curl 'https://app.outboundsync.com/api/v1/sources/10/requests?from=2026-08-01T00:00:00.000Z&to=2026-08-13T23:59:59.999Z' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: RequestsResponse — same shape as GET /api/v1/requests.

Try it in the console

CRM sync attempts for accessible connections — one row per CRM write attempt for a received request (UI: Webhook logs). Distinct from /events, which is the platform webhook log.

Query parameters:

Name Required Description
from yes Start of the range (ISO-8601).
to yes End of the range (ISO-8601).
status no success, warning, or error.
sourceId no Restrict to one source. Unknown or inaccessible ids return 404.
connectionId no Restrict to one connection. An id outside the key’s access returns an empty page — not 404.
cursor no Opaque cursor from a previous page.
limit no Page size (default 100, max 500).

When the key has no accessible connections (and connectionId is omitted), the response is an empty page — not 404.

A status filter classifies rows in batches. If the scan cap is hit before a full page of matches, the response can be a short page with hasMore: true and a continuation cursor so matching rows are not skipped.

curl 'https://app.outboundsync.com/api/v1/syncs?from=2026-08-01T00:00:00.000Z&to=2026-08-13T23:59:59.999Z' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: SyncsResponse

{
"data": [
{
"id": 4401,
"status": "success",
"connectionId": 7,
"sourceId": 10,
"eventType": "EMAIL_SENT",
"toEmail": "alex@acme.com",
"platform": "smartlead",
"payloadId": 9001,
"error": null,
"createdAt": "2026-08-12T15:04:06.000Z"
}
],
"hasMore": false,
"nextCursor": null
}

payloadId is the related request id. status is success, warning, or error. error is the CRM result payload when present, otherwise null.

Try it in the console

Exact counts of CRM sync attempts in the date range (not sampled or capped), split by status. cursor, limit, and status are ignored.

Each sync is classified exactly as its status in GET /api/v1/syncs, so success + warning + error equals the number of syncs listed for the same filters. A sync that never recorded a result (interrupted before completing, or in flight for the few seconds a sync takes) counts as error in both, and it’s the same row GET /api/v1/syncs?status=error returns.

Query parameters: from and to (required), sourceId and connectionId (optional). Same 31-day rule and 404 / empty-page behavior as the list endpoint.

curl 'https://app.outboundsync.com/api/v1/syncs/metrics?from=2026-08-01T00:00:00.000Z&to=2026-08-13T23:59:59.999Z' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: SyncMetrics

{
"success": 120,
"warning": 3,
"error": 5
}
Try it in the console

One sync attempt by id. No from/to query. Returns 404 when that sync is not in the key’s accessible connections.

curl 'https://app.outboundsync.com/api/v1/syncs/4401' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: SyncResource — same object shape as one element of GET /api/v1/syncs data[].

Try it in the console

Enqueue a retry of one CRM sync attempt. No request body. Requires the write scope. Connection-scoped keys can retry (unlike /webhooks*). The write check runs before the sync is loaded, so a read-only key receives 403 even for an unknown id.

Enqueue only — the response does not wait for the CRM write. Retry is supported for HubSpot and Salesforce syncs whose status is error. Success and Warning are not eligible.

curl 'https://app.outboundsync.com/api/v1/syncs/4401/retry' \
  -X POST \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: SyncRetryResponse

{
"queued": true,
"syncId": 4401
}
Status Meaning
200 Queued. syncId is the original sync id.
403 Missing write scope. Message: API key requires the “write” scope.
404 Sync missing or not in the key’s access (Sync 4401 not found).
409 Not Error-eligible (Sync is not eligible for retry); missing webhook or connection context; or CRM is not HubSpot or Salesforce (Retry is only supported for HubSpot and Salesforce syncs).
Try it in the console

Same list as GET /api/v1/syncs, with sourceId taken from the path instead of the query string. Returns 404 when that source is not in the key’s accessible connections.

Query parameters: from and to (required), status, connectionId, cursor, and limit (optional). Do not also pass sourceId as a query parameter.

curl 'https://app.outboundsync.com/api/v1/sources/10/syncs?from=2026-08-01T00:00:00.000Z&to=2026-08-13T23:59:59.999Z' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: SyncsResponse — same shape as GET /api/v1/syncs.

Try it in the console

Outbound delivery attempts to one forwarding destination — one row per HTTP POST OutboundSync sent to that URL. Distinct from Sync Monitoring webhook deliveries.

Query parameters:

Name Required Description
from yes Start of the range (ISO-8601).
to yes End of the range (ISO-8601).
status no success or error.
cursor no Opaque cursor from a previous page.
limit no Page size (default 100, max 500).

Returns 404 (Destination 3 not found) when that destination is missing or its connection is not in the key’s access.

curl 'https://app.outboundsync.com/api/v1/destinations/3/deliveries?from=2026-08-01T00:00:00.000Z&to=2026-08-13T23:59:59.999Z' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: DestinationDeliveriesResponse

{
"data": [
{
"id": 8801,
"destinationId": 3,
"destinationUrl": "https://hooks.example.com/os",
"status": "success",
"httpStatus": 200,
"connectionId": 7,
"sourceId": 10,
"eventType": "EMAIL_SENT",
"toEmail": "alex@acme.com",
"payloadId": 9001,
"error": null,
"createdAt": "2026-08-12T15:04:07.000Z"
}
],
"hasMore": false,
"nextCursor": null
}

payloadId is the related request id. status is success or error (not the Sync Monitoring PENDING / SUCCEEDED / FAILED / DEAD enums). sourceId, eventType, and toEmail are populated for new delivery logs and remain nullable for historical rows written before metadata capture.

GET /api/v1/destinations/:id/deliveries/metrics

Section titled “GET /api/v1/destinations/:id/deliveries/metrics”
Try it in the console

Counts of outbound delivery attempts to one destination in the date range. cursor, limit, and status are ignored.

Query parameters: from and to (required). Same 31-day rule and 404 behavior as the list endpoint.

curl 'https://app.outboundsync.com/api/v1/destinations/3/deliveries/metrics?from=2026-08-01T00:00:00.000Z&to=2026-08-13T23:59:59.999Z' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: DestinationDeliveryMetrics

{
"success": 110,
"error": 4,
"http2xx": 110
}

http2xx counts rows whose recorded HTTP status is in the 2xx range (including legacy rows where status was inferred).

Try it in the console

Account-wide delivery export across all accessible destinations — same row shape as GET /api/v1/destinations/:id/deliveries.

Query parameters:

Name Required Description
from yes Start of the range (ISO-8601).
to yes End of the range (ISO-8601).
status no success or error.
destinationId no Restrict to one destination. Unknown or inaccessible ids return 404.
cursor no Opaque cursor from a previous page.
limit no Page size (default 100, max 500).

When the key has no accessible destinations (and destinationId is omitted), the response is an empty page — not 404.

curl 'https://app.outboundsync.com/api/v1/deliveries?from=2026-08-01T00:00:00.000Z&to=2026-08-13T23:59:59.999Z' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: DestinationDeliveriesResponse — same shape as the nested list.

POST /api/v1/destinations/:id/deliveries/:deliveryId/replay

Section titled “POST /api/v1/destinations/:id/deliveries/:deliveryId/replay”
Try it in the console

Enqueue a replay of one destination delivery. No request body. Requires the write scope. Connection-scoped keys can replay. The write check runs before the delivery is loaded, so a read-only key receives 403 even for an unknown id.

Enqueue only — the response does not wait for the outbound POST. Unlike sync retry, there is no status gate: any accessible delivery that still has a payloadId can be replayed (success or error).

curl 'https://app.outboundsync.com/api/v1/destinations/3/deliveries/8801/replay' \
  -X POST \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: DestinationDeliveryReplayResponse

{
"queued": true,
"destinationId": 3,
"payloadId": 9001
}
Status Meaning
200 Queued. payloadId is the related request id that will be re-forwarded.
403 Missing write scope. Message: API key requires the “write” scope.
404 Destination missing/inaccessible; delivery missing for that destination; or delivery has no payload to replay.
Try it in the console

One-shot rollup for dashboards: inbound request count, CRM sync counts by status, and destination delivery counts for a date range.

Query parameters:

Name Required Description
from yes Start of the range (ISO-8601).
to yes End of the range (ISO-8601).
connectionId no Restrict all three legs to one connection. An id outside the key’s access returns zeros for that scope — not 404.
sourceId no Restrict requests and syncs to one source. Unknown or inaccessible ids return 404. Does not filter destinationDeliveries (that leg stays connection-scoped).

Same 31-day rule as other observability metrics.

curl 'https://app.outboundsync.com/api/v1/account/metrics?from=2026-08-01T00:00:00.000Z&to=2026-08-13T23:59:59.999Z' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: AccountMetricsResponse

{
"from": "2026-08-01T00:00:00.000Z",
"to": "2026-08-13T23:59:59.999Z",
"requests": { "count": 128 },
"syncs": { "success": 120, "warning": 3, "error": 5 },
"destinationDeliveries": { "success": 110, "error": 4, "http2xx": 110 }
}

requests, syncs, and destinationDeliveries use the same shapes as request metrics, sync metrics, and destination delivery metrics. syncs follows the same counting rule as sync metrics: syncs with no recorded result count as error.

This is the heaviest read in the API. If it runs past the 20-second query limit it returns a JSON 504; narrow from/to or pass connectionId. See 504 query time limit.

How-to guides (setup, signatures, retries): OutboundSync webhooks. This section is the API contract.

OutboundSync emits its own events and delivers them to customer-registered HTTPS endpoints. This is distinct from sources (inbound SEP paste URLs — UI Sources) and destinations (Forwarding destinations and Reply relays). Webhooks are account-level (Platform access → Webhooks).

Keep these nouns distinct:

Term ID prefix Meaning
Webhook oswhk_ Registered HTTPS URL + event filter
Event osevt_ Something that happened
Delivery oswhd_ One attempt to POST an event to a webhook
Signing secret oswhsec_ Shown once on create or rotate

All /api/v1/webhooks* and /api/v1/events* routes require:

  1. A valid Bearer API key with API access enabled (canUseApi).
  2. Webhooks enabled for the account (canUseWebhooks) — otherwise these routes return 403. Ask an OutboundSync admin to enable this, the same way API access is enabled.

Additionally:

  • Every /api/v1/webhooks* route (including GETs) requires an account-scoped key. Connection-scoped keys receive 403.
  • Mutations (POST, PATCH, DELETE) require the write scope. Keys default to read; ask OutboundSync support to issue a write-scoped account key when you need to register or manage webhooks programmatically.
  • /api/v1/events* allows connection-scoped keys: they see that connection’s events plus account-level events.

Event type names follow the connection capability model (sync, blocklists, destinations) plus connection lifecycle.

Active / subscribable (v1 — Sync Monitoring):

Event Fires when
sync.failed A source→CRM sync transitions from healthy to failing for that source×connection pair (one alert per incident, not per failed job). A connection-breaking CRM error (auth, billing, app not installed) fires it on the first failure and pauses the sync; other failures fire it after 3 consecutive failed syncs. Temporary errors (CRM rate limits, server and network errors) never count. See Common sync.failed reasons.
sync.recovered A previously failing source→CRM sync starts succeeding again (on the next successful sync)

Test-only (not subscribable): test.ping is delivered only by POST /api/v1/webhooks/:id/test. It cannot be listed in enabledEvents.

Reserved (namespace held; subscribing returns 400 with the list of available types): connection.*, blocklist.*, destination.*, and provisional record-write names. Confirm naming when those events ship.

Try it in the console

Register a webhook. Requires write + account-scoped key.

Request body:

{
"url": "https://hooks.example.com/outboundsync",
"description": "PagerDuty Sync Monitoring",
"enabledEvents": ["sync.failed", "sync.recovered"]
}
  • url (required) — HTTPS URL (max 2048 chars). Private/local targets are rejected.
  • description (optional) — max 500 chars.
  • enabledEvents (optional) — empty or omitted means all subscribable active events (sync.failed, sync.recovered). It does not include test.ping or reserved names. Reserved or unknown types return 400.

Response: WebhookEndpointWithSecret — includes secret (shown once).

{
"id": "oswhk_1a2b3c4d5e6f",
"url": "https://hooks.example.com/outboundsync",
"description": "PagerDuty Sync Monitoring",
"enabledEvents": ["sync.failed", "sync.recovered"],
"isActive": true,
"autoDisabledAt": null,
"createdAt": "2026-07-09T12:00:00.000Z",
"updatedAt": "2026-07-09T12:00:00.000Z",
"secret": "oswhsec_…"
}

Store the secret immediately. An account may register at most 20 webhooks.

Try it in the console

List webhooks for the account. Requires an account-scoped key (read).

curl 'https://app.outboundsync.com/api/v1/webhooks' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: { "webhooks": [...] } — no secrets.

{
"webhooks": [
{
"id": "oswhk_1a2b3c4d5e6f",
"url": "https://hooks.example.com/outboundsync",
"description": "PagerDuty Sync Monitoring",
"enabledEvents": ["sync.failed", "sync.recovered"],
"isActive": true,
"autoDisabledAt": null,
"createdAt": "2026-07-09T12:00:00.000Z",
"updatedAt": "2026-07-09T12:00:00.000Z"
}
]
}
Try it in the console

Get one webhook by id.

curl 'https://app.outboundsync.com/api/v1/webhooks/oswhk_1a2b3c4d5e6f' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'
Try it in the console

Update url, description, enabledEvents, and/or isActive. Requires write. Setting isActive: true clears an auto-disable.

Request body (all fields optional):

{
"isActive": true,
"enabledEvents": ["sync.failed", "sync.recovered"]
}
Try it in the console

Soft-delete a webhook. Requires write. Returns 204 with no body.

Try it in the console

Rotate the signing secret. Requires write. Returns the webhook plus a new secret (shown once).

{
"id": "oswhk_1a2b3c4d5e6f",
"url": "https://hooks.example.com/outboundsync",
"description": "PagerDuty Sync Monitoring",
"enabledEvents": ["sync.failed", "sync.recovered"],
"isActive": true,
"autoDisabledAt": null,
"createdAt": "2026-07-09T12:00:00.000Z",
"updatedAt": "2026-07-09T12:05:00.000Z",
"secret": "oswhsec_…"
}
Try it in the console

Queue a test.ping delivery to this webhook. Requires write. Returns 202:

{
"eventId": "osevt_9f8e7d6c5b4a"
}
Try it in the console

Delivery attempts for a webhook. Filters: status (PENDING | SUCCEEDED | FAILED | DEAD), cursor, limit (default 25, max 100).

curl 'https://app.outboundsync.com/api/v1/webhooks/oswhk_1a2b3c4d5e6f/deliveries' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: cursor page { "data": [...], "hasMore": false, "nextCursor": null }.

{
"data": [
{
"id": "oswhd_abc123",
"endpointId": "oswhk_1a2b3c4d5e6f",
"status": "SUCCEEDED",
"attemptCount": 1,
"lastAttemptAt": "2026-07-09T12:00:01.000Z",
"lastHttpStatus": 200,
"lastError": null,
"replayOfDeliveryId": null,
"createdAt": "2026-07-09T12:00:00.000Z",
"updatedAt": "2026-07-09T12:00:01.000Z"
}
],
"hasMore": false,
"nextCursor": null
}

Webhooks auto-disable after 20 consecutive terminally failed deliveries. Re-enable with PATCH isActive: true.

POST /api/v1/webhooks/:id/deliveries/:deliveryId/replay

Section titled “POST /api/v1/webhooks/:id/deliveries/:deliveryId/replay”
Try it in the console

Replay a delivery; returns the new delivery linked to the original via replayOfDeliveryId. Requires write.

Each delivery is a POST of this JSON body:

{
"id": "osevt_1a2b3c...",
"type": "sync.failed",
"created": "2026-07-09T12:00:00.000Z",
"summary": "Sync smartlead → HubSpot is failing: The CRM connection is missing a valid refresh token. Reconnect the CRM to resume syncing.",
"data": {
"connectionId": 7,
"crm": "HUBSPOT",
"sourceId": 10,
"sourcePlatform": "smartlead",
"consecutiveFailures": 1,
"reason": "The CRM connection is missing a valid refresh token. Reconnect the CRM to resume syncing.",
"remediation": "Your HubSpot connection appears disconnected or its access was revoked. Reconnect HubSpot in the OutboundSync dashboard: https://app.outboundsync.com/admin/dashboard/hubspot — https://outboundsync.com/docs/api/v1#remediation-reconnect"
}
}

Every sync.failed payload pairs reason (what went wrong) with remediation (the exact fix, with a dashboard or docs deep link) — the same remediation voice as connection blockers. An operator, or their agent, can resolve the failure without leaving the payload.

  • summary reads Sync <sourcePlatform> → <CRM> is failing: <reason>, with the CRM’s display name (HubSpot, Salesforce). sync.recovered reads Sync <sourcePlatform> → <CRM> recovered.
  • data.crm stays the enum value (HUBSPOT, SALESFORCE, ATTIO, CLOSE, HIGHLEVEL, PIPEDRIVE).
  • reason is a single readable line: email addresses are redacted to [email], it is capped at 240 characters, and it is never raw JSON.
  • consecutiveFailures is the failure count for the source×connection when the alert fired.

data.reason is the customer-facing line for the classified error. data.remediation comes from one of the seven categories below (HubSpot shown; the CRM name and dashboard path follow the connection). Unknown Salesforce API errors can instead show a sanitized errorCode: message reason.

Category Example reason What it means What to do (remediation)
reconnect The CRM connection is missing a valid refresh token. Reconnect the CRM to resume syncing. OAuth expired, tokens revoked, or the CRM app is not installed. Pauses the sync. Your HubSpot connection appears disconnected or its access was revoked. Reconnect HubSpot in the OutboundSync dashboard.
billing The CRM account is over its plan or billing limit. HubSpot HTTP 402 / PAYMENT_REQUIRED, or a Salesforce storage limit. Pauses the sync. The HubSpot account is over its plan or billing limit. Upgrade the HubSpot plan or free capacity, then retry.
config An API key is not configured for this sales-engagement source. Missing sequencer API key, a required field mapping (profile URL), or an unsupported event type. A required source setting or field mapping is missing. Review the source configuration in the OutboundSync dashboard.
not_found No matching CRM record was found, and create-on-miss is disabled. Update-only mode is on and no Lead or Contact matched. No matching HubSpot record was found, and create-on-miss is disabled. Create the record in HubSpot or turn off update-only mode.
rate_limited The CRM rate-limited this request. The sync will retry automatically. HTTP 429 from the CRM. Not a configuration problem. Check whether the sync was retried automatically. If it’s still Error, retry it.
network_transient The CRM had a temporary error. The sync will retry automatically. Provider 5xx, network errors, a token-refresh or record lock, or an expired Salesforce session (INVALID_SESSION_ID). Check whether the sync was retried automatically. If it’s still Error, retry it.
unknown An unexpected error occurred while syncing this event. An unrecognized or internal error. Review this connection’s recent sync errors in the OutboundSync dashboard. If failures continue, contact support and include the reason.

For rate_limited and network_transient, the live remediation string reads “No action needed — the sync will retry automatically.” That’s only true for the automatically retried cases. In practice you won’t see rate_limited in a sync.failed payload, because temporary errors don’t count toward the alert. network_transient appears only when Salesforce record lock errors keep failing after their automatic retries.

The live remediation string also carries the dashboard link for the connection (https://app.outboundsync.com/admin/dashboard/<crm>, except for the no-action categories) and a link back to the matching row of this table.

A reconnect or billing failure means every later sync for that source×connection would fail the same way. Instead of retrying each event against a broken connection, OutboundSync pauses CRM sync for that pair and fires sync.failed once.

  • While paused: incoming events are still received and logged, but they aren’t written to the CRM. Each appears in GET /api/v1/syncs and the dashboard History as an error sync with the message sync_circuit_open.
  • To resume: reconnect the CRM in the OutboundSync dashboard. That also applies after you fix a billing limit. Reconnecting resumes every paused source on that connection. sync.recovered fires on the next successful sync, not at the moment you reconnect.
  • Events skipped while paused are not replayed automatically. Retry them with POST /api/v1/syncs/:id/retry (HubSpot and Salesforce), or ask OutboundSync support to batch-requeue them. See How to reprocess errored webhook logs.

Temporary errors (CRM rate limits, server and network errors), update-only misses, and a missing sequencer API key never pause a sync; they are handled per event. Only some temporary errors are retried automatically.

Headers: OutboundSync-Event-Id, OutboundSync-Delivery-Id, and OutboundSync-Signature: t=<unix>,v1=<hex>.

  • At-least-once with one initial attempt plus 7 retries. Each attempt times out after 10 seconds. The first retry waits 1 minute plus 0–300 seconds of per-delivery jitter; later retries follow 2, 4, 8, 16, 30, and 60 minutes (same shared policy as Forwarding destinations). Expect duplicate and out-of-order events; dedup on id and order by created.
  • Any 2xx is success. 404 and 413 are terminal for platform-event envelopes; other failures retry. Retries exhausted → dead-lettered (deliveries?status=DEAD) and can be replayed.
  • The /events log is the reconciliation source of truth — pull it to recover anything missed while an endpoint was down. Events remain queryable even if no endpoint was active when they occurred.

The v1 signature is HMAC-SHA256(secret, "<t>.<raw request body>"), hex-encoded. Compare with a constant-time check, and reject timestamps outside a tolerance window (for example 5 minutes).

const crypto = require('crypto');
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const fresh = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t)) <= toleranceSec;
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Try it in the console

Queryable platform event log. Filters: type (repeatable), delivered (true / false), cursor, limit (default 25, max 100). For sync.failed payloads, see Common sync.failed reasons.

curl 'https://app.outboundsync.com/api/v1/events' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: cursor page of events.

{
"data": [
{
"id": "osevt_1a2b3c",
"type": "sync.failed",
"summary": "Sync smartlead → HubSpot is failing: The CRM connection is missing a valid refresh token. Reconnect the CRM to resume syncing.",
"data": {
"connectionId": 7,
"crm": "HUBSPOT",
"sourceId": 10,
"sourcePlatform": "smartlead",
"consecutiveFailures": 1,
"reason": "The CRM connection is missing a valid refresh token. Reconnect the CRM to resume syncing.",
"remediation": "Your HubSpot connection appears disconnected or its access was revoked. Reconnect HubSpot in the OutboundSync dashboard: https://app.outboundsync.com/admin/dashboard/hubspot — https://outboundsync.com/docs/api/v1#remediation-reconnect"
},
"connectionId": 7,
"sourceId": 10,
"delivered": true,
"createdAt": "2026-07-09T12:00:00.000Z"
}
],
"hasMore": false,
"nextCursor": null
}
Try it in the console

One event with its delivery attempts.

curl 'https://app.outboundsync.com/api/v1/events/osevt_1a2b3c' \
  -X GET \
  -H 'Authorization: Bearer osapi_<your-secret>'

Response: event fields plus deliveries: [...] (same shape as the deliveries list items above).

Value Meaning
ready Component is configured and operational (DB-derived)
not_configured Feature enabled but nothing set up yet
disconnected CRM OAuth token missing (refreshToken is null)
disabled Feature not enabled on this connection (plan/capability gate)
error Component has a recorded error (blocklists only)

Blockers are hard failures — sync cannot work. They gate the top-level ready flag. Only these three codes appear in blockers[].

Each blocker includes:

Field Type Description
code BlockerCode Machine-readable identifier
connectionId number Connection this blocker applies to
crm string CRM profile enum (e.g. HUBSPOT, SALESFORCE)
message string Human-readable explanation of what is wrong
remediation string Actionable next step, including dashboard deep links where applicable
docUrl string Link to this documentation section for the blocker code
Code Condition Gates ready
crm_disconnected CRM OAuth token missing Yes
sync_not_enabled canSync is false (account not authorized to sync) Yes
no_sources canSync is true but zero inbound sources configured Yes

Warnings are optional gaps or non-fatal issues. They never gate ready. A connection can be ready: true with warnings present.

Each warning includes the same fields as blockers (code, connectionId, crm, message, remediation, docUrl).

Code Condition Gates ready
destinations_not_configured canUserAccount (Destinations) is true but no forwarding URLs or reply relays bound No
blocklists_not_configured canBlockList is true but no enabled block list No
blocklists_error Enabled block list has lastError set No

When a feature is disabled on the plan (canUserAccount or canBlockList is false), the component status is disabled and no warning is emitted — the customer chose not to use that feature.

Per connection:

crmConnection

  • refreshToken != null → ready
  • refreshToken == null → disconnected + blocker crm_disconnected

sources

destinations (component detail; may also surface as warnings[])

  • canUserAccount == false → disabled (no warning)
  • zero destination bindings (forwarding or reply relay) → not_configured + warning destinations_not_configured
  • ≥1 binding → ready

blocklists (component detail; may also surface as warnings[])

  • canBlockList == false → disabled (no warning)
  • no lists or none enabled → not_configured + warning blocklists_not_configured
  • any enabled list with lastError != null → error + warning blocklists_error
  • otherwise → ready

Connection ready: crmConnection.status === "ready" && sources.status === "ready"

Top-level ready: connections.length > 0 && every(connection.ready)

Responses never include: refreshToken, settings, details, keyHash, apiKeyId, salesfinityWebhookSecret.

refreshToken is read from the database only to derive crmConnected status; the token value is discarded before any response assembly.

Field Endpoint Note
sources[].url /sources Contains the webhook ingress code. Treat API keys like secrets — anyone with read access can obtain paste URLs.
data[].path /requests Source ingress code (same secrecy as sources[].url).
data[].error / error /syncs CRM result payload. May contain vendor API messages.
destinations[].url /sources Customer-configured forwarding URLs. Only returned for bindings owned by the same connection.
destinations[].url / url /destinations Same customer forwarding URLs on the catalog list and get-by-id.
blocklists.lastError /account/status Operational error text from CRM sync jobs. May contain vendor API messages; surfaced in warnings[] when applicable.
lastError /blocklists Same operational error text on each list resource.
account.email /me PII; only returned on the identity endpoint.
secret /webhooks create and rotate-secret Signing secret (oswhsec_…); shown once; never in list or get responses.
Status Meaning
401 Missing/invalid/revoked API key (WWW-Authenticate: Bearer realm="OutboundSync API")
403 Valid key but forbidden — no API-enabled connections (canUseApi), missing write scope, blocklist resync when canBlockList is false, connection-scoped key on /webhooks*, or Webhooks not enabled (canUseWebhooks)
404 Resource not found, or a path that doesn’t exist or isn’t shipped yet (message: "Cannot GET /api/v1/…"). Always JSON, never a 200 HTML page — see 404 unknown route.
429 Per-IP or per-account rate limit exceeded (Retry-After header set; per-account responses also carry X-RateLimit-* headers)
504 Query time limit (20s) exceeded on account/metrics, syncs (list + metrics), sources/:sourceId/syncs, requests/metrics, destinations/:id/deliveries/metrics, or contacts/outreach. The work is cancelled and the body is the standard JSON error with a hint. Narrow from/to or pass connectionId, or retry shortly — see 504 query time limit.

API keys are scoped to accessibleConnectionIds resolved at auth time. Connection-scoped keys see exactly one connection. All list queries re-apply deletedAt: null on connections. See Errors and rate limits for the full error contract and retry guidance.

  • Introspection and observability read endpoints (/me, /account/status, /account/metrics, /connections, /sources, /destinations, /requests, /syncs, /deliveries, /blocklists) remain read-only and DB-derived; no live CRM calls that would expand the data processing boundary.
  • Writes: POST /api/v1/syncs/:id/retry, POST /api/v1/destinations/:id/deliveries/:deliveryId/replay, and blocklist pause / resync (enqueue CRM refetch) require the write scope; connection-scoped keys may call them. The /webhooks surface introduces writes: endpoint registration, secret rotation, test, and delivery replay. Those require the write scope and an account-scoped key.
  • Scope enforcement is applied per route: mutations require write. Keys default to read; write must be requested when the key is created. Sync retry, destination replay, and blocklist pause/resync reject keys without write before loading the target resource.
  • Signing secrets are stored encrypted at rest (AES-256-GCM); the plaintext is shown once on create/rotate and never in list responses.

Not shipped yet. Calling a deferred route returns a JSON 404 (for example Cannot GET /api/v1/blocklists/1/entries).

  • lastEventReceivedAt on sources (requires WebhookLog index migration)
  • POST /api/v1/connections/:id/test (live CRM token probe)
  • GET /api/v1/blocklists/:id/entries
  • DELETE /api/v1/blocklists/:id/entries (clear local OS entries; does not imply SEP wipe)
  • Reply-relay create / update / bind under /api/v1/destinations/reply-relays
Using the OutboundSync API v1 (https://outboundsync.com/docs/api/v1/), write a function that:
1. Reads OUTBOUNDSYNC_API_KEY from the environment.
2. Calls GET https://app.outboundsync.com/api/v1/account/status with Bearer authentication.
3. Returns `ready`, plus any blockers[] and warnings[] (code, message, remediation).
Handle 401, 403, 404, 429, and 504 per https://outboundsync.com/docs/api/errors-and-rate-limits/.