API reference (v1)
Contacts
The tenant-facing contact push, inbound webhook and link-minting operations.
mintContactLink
POST /api/contacts/links Mint a token and a tagged link for one contact.
For platforms that generate the link at send time. Names the contact by contactId, or both source and externalId (contactId takes precedence if both are sent). Requires the contacts:links:write scope, which is NOT granted by default to a new key (a key carrying only contacts:write is 403 here). A same-origin portal session is also accepted in place of a key; that first-party path is out of scope for this document.
Authentication: ApiKeyAuthBearer or ApiKeyAuthHeader
Request body (application/json, required)
Fields:
contactId(string): The contact's id, a UUID.destinationUrl(string, required): Must resolve to a domain this tenant has authorized.expSeconds(integer): Only meaningful for type=signed.externalId(string)source(string)type(enum "opaque", "signed")
Responses
- Response
200: The minted token and its tagged link. - Response
400: Malformed JSON, a validation failure (issues carries the validation issues), an unauthorized destination host, or an invalid destination URL. - Response
401: No key and no portal session, or a present key that fails validation. - Response
403: The key is valid but lacks the contacts:links:write scope (not granted by default). - Response
404: No contact matches the given contactId, or the (source, externalId) pair. - Response
429: A per-customer fixed-window limit (120/min), or on the machine-auth path a per-key per-minute or daily limit; honor Retry-After. - Response
500: An unclassified mint failure. - Response
503: type=signed was requested but signed links are temporarily unavailable.
pushContacts
POST /v1/contacts Push a batch of contacts (the canonical bearer-authenticated ingest).
Idempotent per account on (source, external_id): a re-pushed batch refreshes the existing rows rather than duplicating them. Requires the contacts:write scope. API access is a plan feature; keys are issued, rotated and revoked from the portal's Inbound Connections screen.
Authentication: ApiKeyAuthBearer or ApiKeyAuthHeader
Request body (application/json, required): Accepts the narrow {contacts:[...]} shape. At most 10000 records per call (a larger batch is 413), and the whole body is capped at 8388608 bytes. A nested {events:[{resolution:{...}}]} shape some inbound connectors send is accepted only by the deprecated portal-origin fallback for this operation, never by this host; a body with no contacts array here is a 400.
Fields:
contacts(array of object, required)
Responses
- Response
202: 202 Accepted: the batch is durably queued and not yet upserted. Ingestion completes shortly after. - Response
400: Malformed JSON, or a body with no contacts array. - Response
401: The presented key is missing, unknown, malformed, expired, or past its revocation grace. The response never reveals which. - Response
403: The key is not permitted to perform this operation (for example, it lacks the contacts:write scope). - Response
404: The {customerPublicId} subdomain is unknown or inactive. - Response
413: More than 10000 records in one call, or a body over 8388608 bytes. - Response
429: Too many requests for this API key: more than 120 in a 60-second window, or more than 50,000 in a 24-hour window. Wait the number of seconds in Retry-After, then retry: 60 for the short window, or the seconds left in the 24-hour window. Requests that fail authentication or routing do not count toward the limit. - Response
503: The queue producer failed to accept the message. Retry is safe: the eventual upsert is idempotent.
pushHighLevelContact
POST /v1/contacts/highlevel The HighLevel native inbound webhook.
Authenticated by the connection's per-source token, sent as the ?token= query parameter of the webhook URL the portal shows (or the x-webhook-token header) and constant-time compared against the hash stored for the body's HighLevel location. A present token that does not match answers 401 in every case. The {customerPublicId} subdomain selects the account, and the body locationId (or location_id) must be an active HighLevel location connected to that account. Transition: until every connection has moved to the tokenized URL, a call that carries no token at all is still accepted and flagged to OSPRY; after the cutover it answers 401, so re-paste the webhook URL from the portal now. The HighLevel contact_id becomes external_id (source=highlevel), matching the idempotency anchor POST /v1/contacts and POST /v1/contacts/{sourceId} use.
Authentication: SourceTokenHeader or SourceTokenQuery
Request body (application/json, required)
Fields:
company(string | null)companyName(string | null)contactId(string)contact_id(string)email(string | null)firstName(string | null)first_name(string | null)id(string): A HighLevel contact id, used as external_id when contactId/contact_id are absent.lastName(string | null)last_name(string | null)locationId(string)location_id(string)title(string | null)
Responses
- Response
202: 202 Accepted: the batch is durably queued and not yet upserted. Ingestion completes shortly after. - Response
400: Malformed JSON, or the body carries no contact id. - Response
401: A token was presented and does not match the stored hash for the body's HighLevel location (including a token that belongs to another connection or account), or, after the tokenized-URL cutover, no token was presented. The response never reveals which. - Response
404: The {customerPublicId} subdomain is unknown or inactive, the account has no active HighLevel connection, or the body locationId is missing or is not an active HighLevel location of this account. Answered identically to an unknown subdomain, revealing nothing about whether HighLevel is configured. - Response
413: A body over 8388608 bytes. - Response
429: Too many requests for this HighLevel location: more than 120 in a 60-second window, or more than 50,000 in a 24-hour window. Wait the number of seconds in Retry-After, then retry: 60 for the short window, or the seconds left in the 24-hour window. Requests that fail authentication or routing do not count toward the limit. - Response
503: The queue producer failed to accept the message. Retry is safe: the eventual upsert is idempotent.
pushContactsBySource
POST /v1/contacts/{sourceId} The generic per-source inbound webhook (Zapier, Make, n8n, a custom script).
Resolves the tenant by {sourceId} plus the presented per-source token, constant-time compared against the stored hash. Accepts the same {contacts:[...]} body as POST /v1/contacts; each record is mapped through this source's field mappings.
Authentication: SourceTokenHeader or SourceTokenQuery
Parameters
- Parameter
sourceId(path, string, required): The per-source id of the connection, as shown in its webhook URL in the portal. Selects which per-source token and field-mapping set authenticate and shape this call.
Request body (application/json, required): Accepts the narrow {contacts:[...]} shape. At most 10000 records per call (a larger batch is 413), and the whole body is capped at 8388608 bytes. A nested {events:[{resolution:{...}}]} shape some inbound connectors send is accepted only by the deprecated portal-origin fallback for this operation, never by this host; a body with no contacts array here is a 400.
Fields:
contacts(array of object, required)
Responses
- Response
202: 202 Accepted: the batch is durably queued and not yet upserted. Ingestion completes shortly after. - Response
400: Malformed JSON, or a body with no contacts array. - Response
401: The source id is unknown, inactive, not a webhook-type source, or the presented token does not match its stored hash. The response never reveals which. - Response
403: Edge only: the source belongs to a different tenant than the {customerPublicId} subdomain. - Response
404: The {customerPublicId} subdomain is unknown or inactive. - Response
413: More than 10000 records in one call, or a body over 8388608 bytes. - Response
429: Too many requests for this source: more than 120 in a 60-second window, or more than 50,000 in a 24-hour window. Wait the number of seconds in Retry-After, then retry: 60 for the short window, or the seconds left in the 24-hour window. Requests that fail authentication or routing do not count toward the limit. - Response
503: The queue producer failed to accept the message. Retry is safe: the eventual upsert is idempotent.