Skip to content

API v1 reference

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). For 401 / 403 / 429 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):

ConceptSchema todayUI label todayAPI termPath
Inbound URLs sequencers POST events to (POST /webhooks/:code)Webhook model”Sources”sourceGET /api/v1/sources
One inbound POST accepted at a sourceSmartleadPayloadrequestGET /api/v1/requests
One CRM write attempt for a received requestWebhookLog + WebhookLogResult”Sync history”syncGET /api/v1/syncs
Customer URLs OutboundSync forwards received events toUserAccount + UserAccountWebhook junction”Forwarding destinations”destination (forwarding)GET /api/v1/destinations
One outbound POST attempt to a destinationUserAccountWebhookLogsdeliveryGET /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.

NamespaceNowLater
/api/v1/account/*status, metricsusage, limits; /me stays as key-identity check
/api/v1/connectionslist w/ embedded status/:id, /:id/test, /:id/properties
/api/v1/contactsoutreach (prior-outreach summary by email and/or social profile URL)timeline, batch
/api/v1/accountsreservedoutreach (prior-engagement rollup by company domain)
/api/v1/sourceslist w/ config + destinations summary/:id, /:id/logs, POST create
/api/v1/requestsinbound receipts (cursor + date range)
/api/v1/syncsCRM sync attempts (cursor + date range + retry)
/api/v1/destinationslist + get + deliveries + replaycreate/update
/api/v1/destinations/reply-relayslist reply relay catalogcreate/update/bind
/api/v1/deliveriesaccount-wide delivery export (destinationId filter)
/api/v1/blocklistsreservedlist, /:id/entries
/api/v1/webhooksCRUD, rotate-secret, test, deliveries, replayadditional event types
/api/v1/eventsqueryable event logsubscription 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.

Account 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). Webhook and event routes are documented with request samples; console deep-links for those paths arrive when OpenAPI lists them.

MethodPathDescription
GET/api/v1/openapi.jsonOpenAPI 3.1 document (JSON)
GET/api/v1/openapi.yamlOpenAPI 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 currently covers discovery, platform health, and the introspection endpoints on this page (me, account/status, connections, sources, contacts/outreach).

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:

LayerFieldMeaning
Red lightblockers[]Sync cannot work until resolved. Gates ready.
Yellow lightwarnings[]Optional features not configured or non-fatal issues. Never gates ready.
Detailconnections[].* per-component statusEmbedded 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):

ParamRequiredDescription
emailif no profileUrlContact email (normalized to lowercase).
profileUrlif no emailLinkedIn (/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 pathGET /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.

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:

NameRequiredDescription
fromyesStart of the range (ISO-8601).
toyesEnd of the range (ISO-8601).
sourceIdnoRestrict to one source. Unknown or inaccessible ids return 404.
cursornoOpaque cursor from a previous page.
limitnoPage 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:

NameRequiredDescription
fromyesStart of the range (ISO-8601).
toyesEnd of the range (ISO-8601).
statusnosuccess, warning, or error.
sourceIdnoRestrict to one source. Unknown or inaccessible ids return 404.
connectionIdnoRestrict to one connection. An id outside the key’s access returns an empty page — not 404.
cursornoOpaque cursor from a previous page.
limitnoPage 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

Counts of CRM sync attempts in the date range, split by status. cursor, limit, and status are ignored.

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[].

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. Console deep-links arrive when OpenAPI lists this path.

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

Response: SyncRetryResponse

{
"queued": true,
"syncId": 4401
}
StatusMeaning
200Queued. syncId is the original sync id.
403Missing write scope. Message: API key requires the “write” scope.
404Sync missing or not in the key’s access (Sync 4401 not found).
409Not 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:

NameRequiredDescription
fromyesStart of the range (ISO-8601).
toyesEnd of the range (ISO-8601).
statusnosuccess or error.
cursornoOpaque cursor from a previous page.
limitnoPage 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:

NameRequiredDescription
fromyesStart of the range (ISO-8601).
toyesEnd of the range (ISO-8601).
statusnosuccess or error.
destinationIdnoRestrict to one destination. Unknown or inaccessible ids return 404.
cursornoOpaque cursor from a previous page.
limitnoPage 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”

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). Console deep-links arrive when OpenAPI lists this path.

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
}
StatusMeaning
200Queued. payloadId is the related request id that will be re-forwarded.
403Missing write scope. Message: API key requires the “write” scope.
404Destination 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:

NameRequiredDescription
fromyesStart of the range (ISO-8601).
toyesEnd of the range (ISO-8601).
connectionIdnoRestrict all three legs to one connection. An id outside the key’s access returns zeros for that scope — not 404.
sourceIdnoRestrict 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.

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 (Dashboard → Webhooks).

Keep these nouns distinct:

TermID prefixMeaning
Webhookoswhk_Registered HTTPS URL + event filter
Eventosevt_Something that happened
Deliveryoswhd_One attempt to POST an event to a webhook
Signing secretoswhsec_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):

EventFires when
sync.failedA source→CRM sync transitions from healthy to failing after 3 consecutive failed syncs for that source×connection pair (one alert per incident, not per failed job)
sync.recoveredA previously failing source→CRM sync starts succeeding again

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.

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.

List webhooks for the account. Requires an account-scoped key (read). Console Try it deep-links for this path arrive when OpenAPI lists it.

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"
}
]
}

Get one webhook by id.

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

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"]
}

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

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_…"
}

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

{
"eventId": "osevt_9f8e7d6c5b4a"
}

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”

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: invalid_grant",
"data": {
"connectionId": 7,
"crm": "HUBSPOT",
"sourceId": 10,
"sourcePlatform": "smartlead",
"reason": "invalid_grant",
"remediation": "Your HubSpot connection appears disconnected or its access was revoked. Reconnect HubSpot in the OutboundSync dashboard: https://app.outboundsync.com/admin/dashboard/hubspot"
}
}

Every sync.failed payload pairs reason (what went wrong) with remediation (the exact fix, with a dashboard link) — the same remediation voice as connection blockers.

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));
}

Queryable platform event log. Filters: type (repeatable), delivered (true / false), cursor, limit (default 25, max 100). Console Try it deep-links for this path arrive when OpenAPI lists it.

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: invalid_grant",
"data": {
"connectionId": 7,
"crm": "HUBSPOT",
"sourceId": 10,
"sourcePlatform": "smartlead",
"reason": "invalid_grant",
"remediation": "Reconnect HubSpot in the OutboundSync dashboard."
},
"connectionId": 7,
"sourceId": 10,
"delivered": true,
"createdAt": "2026-07-09T12:00:00.000Z"
}
],
"hasMore": false,
"nextCursor": null
}

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).

ValueMeaning
readyComponent is configured and operational (DB-derived)
not_configuredFeature enabled but nothing set up yet
disconnectedCRM OAuth token missing (refreshToken is null)
disabledFeature not enabled on this connection (plan/capability gate)
errorComponent 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:

FieldTypeDescription
codeBlockerCodeMachine-readable identifier
connectionIdnumberConnection this blocker applies to
crmstringCRM profile enum (e.g. HUBSPOT, SALESFORCE)
messagestringHuman-readable explanation of what is wrong
remediationstringActionable next step, including dashboard deep links where applicable
docUrlstringLink to this documentation section for the blocker code
CodeConditionGates ready
crm_disconnectedCRM OAuth token missingYes
sync_not_enabledcanSync is false (account not authorized to sync)Yes
no_sourcescanSync is true but zero inbound sources configuredYes

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).

CodeConditionGates ready
destinations_not_configuredcanUserAccount (Destinations) is true but no forwarding URLs or reply relays boundNo
blocklists_not_configuredcanBlockList is true but no enabled block listNo
blocklists_errorEnabled block list has lastError setNo

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 != nullready
  • refreshToken == nulldisconnected + blocker crm_disconnected

sources

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

  • canUserAccount == falsedisabled (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 == falsedisabled (no warning)
  • no lists or none enabled → not_configured + warning blocklists_not_configured
  • any enabled list with lastError != nullerror + 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.

FieldEndpointNote
sources[].url/sourcesContains the webhook ingress code. Treat API keys like secrets — anyone with read access can obtain paste URLs.
data[].path/requestsSource ingress code (same secrecy as sources[].url).
data[].error / error/syncsCRM result payload. May contain vendor API messages.
destinations[].url/sourcesCustomer-configured forwarding URLs. Only returned for bindings owned by the same connection.
destinations[].url / url/destinationsSame customer forwarding URLs on the catalog list and get-by-id.
blocklists.lastError/account/statusOperational error text from CRM sync jobs. May contain vendor API messages; surfaced in warnings[] when applicable.
account.email/mePII; only returned on the identity endpoint.
secret/webhooks create and rotate-secretSigning secret (oswhsec_…); shown once; never in list or get responses.
StatusMeaning
401Missing/invalid/revoked API key (WWW-Authenticate: Bearer realm="OutboundSync API")
403Valid key but forbidden — no API-enabled connections (canUseApi), missing write scope, connection-scoped key on /webhooks*, or Webhooks not enabled (canUseWebhooks)
429Per-IP or per-account rate limit exceeded (Retry-After header set)

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) remain read-only and DB-derived; no live CRM calls that would expand the data processing boundary.
  • Writes: POST /api/v1/syncs/:id/retry and POST /api/v1/destinations/:id/deliveries/:deliveryId/replay are enqueue only and require the write scope; connection-scoped keys may call both. 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.
  • Signing secrets are stored encrypted at rest (AES-256-GCM); the plaintext is shown once on create/rotate and never in list responses.
  • lastEventReceivedAt on sources (requires WebhookLog index migration)
  • POST /api/v1/connections/:id/test (live CRM token probe)
  • GET /api/v1/blocklists
  • 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, and 429 per https://outboundsync.com/docs/api/errors-and-rate-limits/.