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>Making requests
Section titled “Making requests”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/v1Introspection 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.
Vocabulary
Section titled “Vocabulary”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.
/eventsis that platform log; inbound receipts live under/requestsand 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 onGET /sources(sources[].destinations[]).- The physical inbound route
/webhooks/:codeis unchanged — it is the value of a source’surlfield. Legacy sources created before code-based routing use/webhooks/:hubId/:ownerId; theurlfield always reflects the correct form.
Namespace convention
Section titled “Namespace convention”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 | reserved | list, /:id/entries |
/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
Section titled “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.
Discovery (auth-free)
Section titled “Discovery (auth-free)”| 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.jsonhttps://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"GET /api/v1/me
Section titled “GET /api/v1/me”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.
GET /api/v1/account/status
Section titled “GET /api/v1/account/status”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.
GET /api/v1/connections
Section titled “GET /api/v1/connections”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" } ]}GET /api/v1/sources
Section titled “GET /api/v1/sources”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" } ]}GET /api/v1/contacts/outreach
Section titled “GET /api/v1/contacts/outreach”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.comGET /api/v1/contacts/outreach?profileUrl=https://www.linkedin.com/in/jane-doeGET /api/v1/contacts/outreach?email=jane@acme.com&profileUrl=https://linkedin.com/in/jane-doe&profileUrl=https://x.com/janecurl '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+ everyprofileUrl) 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_emailwith the profile URL when the lead has no email, soprofileUrllookups resolve for those connections. Attio, Close, HighLevel, and Pipedrive leaveto_emailempty for social-only events, so aprofileUrllookup will not match them today — passemailwhen you have it. - Category signal (
outcomes.lastCategory*) matcheslead_email/lead_identity; those rows are usually email-keyed. Preferemailfor category outcomes. - Counts are deduped by inbound payload id per event type × platform (CRM retry duplicates).
everContactedis true for send / reply / call / outbound social message — not open/click-only or passive views/likes.foundis true when webhook log rows and/or a lead category signal exist for the identities.summary.daysSinceLastTouchis whole days sincelastTouchAt(floor), ornullwhen there is no last touch — useful for cadence filters without date math. Category-only matches setfound: truewithdaysSinceLastTouch: null(still eligible under a “null or ≥ N days” cadence filter).doNotContactreports hard signals only:bounced,unsubscribed.blocklistsis a reserved CRM blocklist namespace. In this releaseevaluatedis alwaysfalse,matchedis alwaysfalse, andmatchesis 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 asemailorprofileUrlon 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.emailfor field mapping. Top-levelemailis a deprecated alias ofquery.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).
GET /api/v1/destinations
Section titled “GET /api/v1/destinations”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.
GET /api/v1/destinations/:id
Section titled “GET /api/v1/destinations/:id”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[].
GET /api/v1/destinations/reply-relays
Section titled “GET /api/v1/destinations/reply-relays”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.
Pipeline observability
Section titled “Pipeline observability”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.
GET /api/v1/requests
Section titled “GET /api/v1/requests”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.
GET /api/v1/requests/metrics
Section titled “GET /api/v1/requests/metrics”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}GET /api/v1/sources/:sourceId/requests
Section titled “GET /api/v1/sources/:sourceId/requests”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.
GET /api/v1/syncs
Section titled “GET /api/v1/syncs”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.
GET /api/v1/syncs/metrics
Section titled “GET /api/v1/syncs/metrics”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}GET /api/v1/syncs/:id
Section titled “GET /api/v1/syncs/:id”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[].
POST /api/v1/syncs/:id/retry
Section titled “POST /api/v1/syncs/:id/retry”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}| 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). |
GET /api/v1/sources/:sourceId/syncs
Section titled “GET /api/v1/sources/:sourceId/syncs”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.
GET /api/v1/destinations/:id/deliveries
Section titled “GET /api/v1/destinations/:id/deliveries”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”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).
GET /api/v1/deliveries
Section titled “GET /api/v1/deliveries”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”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}| 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. |
GET /api/v1/account/metrics
Section titled “GET /api/v1/account/metrics”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.
Webhooks (Sync Monitoring)
Section titled “Webhooks (Sync Monitoring)”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:
| 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 |
Prerequisites
Section titled “Prerequisites”All /api/v1/webhooks* and /api/v1/events* routes require:
- A valid Bearer API key with API access enabled (
canUseApi). - Webhooks enabled for the account (
canUseWebhooks) — otherwise these routes return403. 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 receive403. - Mutations (
POST,PATCH,DELETE) require thewritescope. Keys default toread; 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.
Available events
Section titled “Available 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 after 3 consecutive failed syncs for that source×connection pair (one alert per incident, not per failed job) |
sync.recovered | A 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.
POST /api/v1/webhooks
Section titled “POST /api/v1/webhooks”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 includetest.pingor reserved names. Reserved or unknown types return400.
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.
GET /api/v1/webhooks
Section titled “GET /api/v1/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 /api/v1/webhooks/:id
Section titled “GET /api/v1/webhooks/:id”Get one webhook by id.
curl 'https://app.outboundsync.com/api/v1/webhooks/oswhk_1a2b3c4d5e6f' \
-X GET \
-H 'Authorization: Bearer osapi_<your-secret>'PATCH /api/v1/webhooks/:id
Section titled “PATCH /api/v1/webhooks/:id”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"]}DELETE /api/v1/webhooks/:id
Section titled “DELETE /api/v1/webhooks/:id”Soft-delete a webhook. Requires write. Returns 204 with no body.
POST /api/v1/webhooks/:id/rotate-secret
Section titled “POST /api/v1/webhooks/:id/rotate-secret”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_…"}POST /api/v1/webhooks/:id/test
Section titled “POST /api/v1/webhooks/:id/test”Queue a test.ping delivery to this webhook. Requires write. Returns 202:
{ "eventId": "osevt_9f8e7d6c5b4a"}GET /api/v1/webhooks/:id/deliveries
Section titled “GET /api/v1/webhooks/:id/deliveries”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.
Delivery envelope
Section titled “Delivery envelope”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>.
Delivery semantics
Section titled “Delivery semantics”- 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
idand order bycreated. - Any
2xxis success.404and413are terminal for platform-event envelopes; other failures retry. Retries exhausted → dead-lettered (deliveries?status=DEAD) and can be replayed. - The
/eventslog 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.
Verifying the signature
Section titled “Verifying the signature”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));}Events
Section titled “Events”GET /api/v1/events
Section titled “GET /api/v1/events”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}GET /api/v1/events/:id
Section titled “GET /api/v1/events/:id”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).
Status enums
Section titled “Status enums”ComponentStatus
Section titled “ComponentStatus”| 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) |
BlockerCode
Section titled “BlockerCode”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 |
WarningCode
Section titled “WarningCode”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).
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.
Readiness derivation
Section titled “Readiness derivation”Per connection:
crmConnection
refreshToken != null→readyrefreshToken == null→disconnected+ blockercrm_disconnected
sources
canSync == false→disabled+ blockersync_not_enabledcanSync && count == 0→not_configured+ blockerno_sources(create a Source under Dashboard → Sources: https://app.outboundsync.com/admin/dashboard/sources)count > 0→ready
destinations (component detail; may also surface as warnings[])
canUserAccount == false→disabled(no warning)- zero destination bindings (forwarding or reply relay) →
not_configured+ warningdestinations_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+ warningblocklists_not_configured - any enabled list with
lastError != null→error+ warningblocklists_error - otherwise →
ready
Connection ready: crmConnection.status === "ready" && sources.status === "ready"
Top-level ready: connections.length > 0 && every(connection.ready)
Security
Section titled “Security”Serialization allowlist
Section titled “Serialization allowlist”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.
Sensitive fields by design
Section titled “Sensitive fields by design”| 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. |
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. |
Authentication and rate limits
Section titled “Authentication and rate limits”| 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, connection-scoped key on /webhooks*, or Webhooks not enabled (canUseWebhooks) |
429 | Per-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.
SOC 2 notes
Section titled “SOC 2 notes”- 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/retryandPOST /api/v1/destinations/:id/deliveries/:deliveryId/replayare enqueue only and require thewritescope; connection-scoped keys may call both. The/webhookssurface introduces writes: endpoint registration, secret rotation, test, and delivery replay. Those require thewritescope and an account-scoped key. - Scope enforcement is applied per route: mutations require
write. Keys default toread;writemust 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.
Deferred
Section titled “Deferred”lastEventReceivedAton sources (requiresWebhookLogindex 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
Ask an AI coding assistant
Section titled “Ask an AI coding assistant”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/.