Delivery Integration¶
Use the delivery sync endpoint only for legacy profile-wide CRM sync / drip (recurring “more companies from this search”). It is not how you read an order.
Do not use this endpoint to reconcile an order
For one order, retrieve the immutable snapshot only from
GET /api/v1/orders/{order_id}/results.
GET /api/v1/deliveries/{prospect_search_id}/companies is profile-wide
legacy CRM sync / drip. It can include historical or unrelated orders and
will leave the public API-key contract in a later version.
Endpoint¶
GET /api/v1/deliveries/{prospect_search_id}/companies
Required scopes: companies:read and prospects:read.
Website markdown for a delivered company is
GET /api/v1/deliveries/{prospect_search_id}/companies/{company_id}/website-markdown
and requires delivery:read.
This endpoint returns delivered companies with embedded prospects and pagination
metadata. Use delivered_since for incremental syncs.
Set OPENPROSPECT_API_KEY, PROSPECT_SEARCH_ID, and COMPANY_ID before running
the examples. COMPANY_ID is a company id from the delivery list.
For one completed, published order, use the order-scoped result endpoint
documented in the
Enrichment Quick Start.
Query Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
delivered_since |
ISO 8601 datetime | No | Return companies delivered after this timestamp |
limit |
integer | No | Maximum companies to return, from 1 to 100; default 50 |
offset |
integer | No | Number of companies to skip; default 0 |
Pull Delivered Companies¶
This endpoint uses offset pagination over a live, newest-first result set. New
deliveries can arrive between page requests and shift later offsets, so a
scheduled sync can receive the same company more than once. Treat delivery
pages as at-least-once input: idempotently upsert companies by their stable
id and nested prospects by their stable id instead of blindly appending
rows. The examples below deduplicate companies in memory; persist the same
upsert behavior in your CRM or warehouse.
This example uses jq to read pagination metadata and emit one company per
line. Replace the final jq command with an idempotent upsert keyed by
.id.
offset=0
while true; do
if ! page="$(
curl --fail-with-body -sS \
"https://api.openprospect.io/api/v1/deliveries/${PROSPECT_SEARCH_ID}/companies?limit=100&offset=${offset}" \
-H "Authorization: Bearer ${OPENPROSPECT_API_KEY}"
)"; then
printf 'Delivery request failed; pagination stopped.\n' >&2
exit 1
fi
jq -c '.items[]' <<<"${page}"
has_more="$(jq -r '.has_more' <<<"${page}")"
item_count="$(jq '.items | length' <<<"${page}")"
if [[ "${has_more}" == "false" ]]; then
break
fi
offset=$((offset + item_count))
done
import os
import httpx
api_key = os.environ["OPENPROSPECT_API_KEY"]
prospect_search_id = os.environ["PROSPECT_SEARCH_ID"]
companies_by_id = {}
offset = 0
while True:
response = httpx.get(
f"https://api.openprospect.io/api/v1/deliveries/{prospect_search_id}/companies",
headers={"Authorization": f"Bearer {api_key}"},
params={"limit": 100, "offset": offset},
timeout=30.0,
)
response.raise_for_status()
page = response.json()
companies_by_id.update({company["id"]: company for company in page["items"]})
if not page["has_more"]:
break
offset += len(page["items"])
print(f"Retrieved {len(companies_by_id)} unique companies")
const apiKey = process.env.OPENPROSPECT_API_KEY;
const prospectSearchId = process.env.PROSPECT_SEARCH_ID;
if (!apiKey || !prospectSearchId) {
throw new Error("OPENPROSPECT_API_KEY and PROSPECT_SEARCH_ID must be set");
}
const companiesById = new Map();
let offset = 0;
while (true) {
const url = new URL(
`https://api.openprospect.io/api/v1/deliveries/${prospectSearchId}/companies`,
);
url.searchParams.set("limit", "100");
url.searchParams.set("offset", String(offset));
const response = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) {
throw new Error(`Delivery sync failed: ${response.status} ${await response.text()}`);
}
const page = await response.json();
for (const company of page.items) {
companiesById.set(company.id, company);
}
if (!page.has_more) {
break;
}
offset += page.items.length;
}
console.log(`Retrieved ${companiesById.size} unique companies`);
interface DeliveredProspect {
id: string;
company_id: string;
first_name: string;
email: string | null;
email_status: string | null;
email_type: string | null;
email_delivery_quality: string | null;
email_delivery_quality_reason: string | null;
email_client_deliverable: boolean;
}
interface DeliveredCompany {
id: string;
name: string;
source_id: string | null;
prospects: DeliveredProspect[];
}
interface DeliveredCompaniesResponse {
items: DeliveredCompany[];
total: number;
limit: number;
offset: number;
has_more: boolean;
}
const apiKey = process.env.OPENPROSPECT_API_KEY;
const prospectSearchId = process.env.PROSPECT_SEARCH_ID;
if (!apiKey || !prospectSearchId) {
throw new Error("OPENPROSPECT_API_KEY and PROSPECT_SEARCH_ID must be set");
}
const companiesById = new Map<string, DeliveredCompany>();
let offset = 0;
while (true) {
const url = new URL(
`https://api.openprospect.io/api/v1/deliveries/${prospectSearchId}/companies`,
);
url.searchParams.set("limit", "100");
url.searchParams.set("offset", String(offset));
const response = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) {
throw new Error(`Delivery sync failed: ${response.status} ${await response.text()}`);
}
const page = (await response.json()) as DeliveredCompaniesResponse;
for (const company of page.items) {
companiesById.set(company.id, company);
}
if (!page.has_more) {
break;
}
offset += page.items.length;
}
console.log(`Retrieved ${companiesById.size} unique companies`);
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
var apiKey = Environment.GetEnvironmentVariable("OPENPROSPECT_API_KEY")
?? throw new InvalidOperationException("OPENPROSPECT_API_KEY is not set.");
var prospectSearchId = Environment.GetEnvironmentVariable("PROSPECT_SEARCH_ID")
?? throw new InvalidOperationException("PROSPECT_SEARCH_ID is not set.");
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
var companiesById = new Dictionary<string, JsonElement>();
var offset = 0;
while (true)
{
var url =
$"https://api.openprospect.io/api/v1/deliveries/{prospectSearchId}/companies"
+ $"?limit=100&offset={offset}";
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
using var page = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var items = page.RootElement.GetProperty("items").EnumerateArray().ToArray();
foreach (var item in items)
{
var company = item.Clone();
companiesById[company.GetProperty("id").GetString()!] = company;
}
if (!page.RootElement.GetProperty("has_more").GetBoolean())
{
break;
}
offset += items.Length;
}
Console.WriteLine($"Retrieved {companiesById.Count} unique companies");
Expected response:
The embedded prospect includes email_status, email_type,
email_delivery_quality, email_delivery_quality_reason, and
email_client_deliverable.
{
"items": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Grand Hotel Berlin",
"website_url": "https://www.grandhotel-berlin.de",
"source_id": "crm-1001",
"prospects": [
{
"id": "e5f6a7b8-c9d0-1234-e5f6-a7b8c9d01234",
"company_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"first_name": "Maria",
"last_name": "Schmidt",
"email": "m.schmidt@grandhotel-berlin.de",
"job_title": "General Manager",
"email_status": "VERIFIED",
"email_type": "PERSONAL",
"email_delivery_quality": "client_deliverable",
"email_delivery_quality_reason": "verified_personal_email",
"email_client_deliverable": true
}
]
}
],
"total": 1,
"limit": 50,
"offset": 0,
"has_more": false
}
Keep prospects whose email_status == "VERIFIED" when your integration accepts
only verified emails. Use email_client_deliverable == true when you want
OpenProspect's stricter default-campaign policy decision.
Website Markdown¶
Optimized website markdown for a delivered company requires delivery:read.
curl --fail-with-body -sS \
"https://api.openprospect.io/api/v1/deliveries/${PROSPECT_SEARCH_ID}/companies/${COMPANY_ID}/website-markdown" \
-H "Authorization: Bearer ${OPENPROSPECT_API_KEY}"
Expected response:
{
"company_id": "550e8400-e29b-41d4-a716-446655440001",
"prospect_search_id": "550e8400-e29b-41d4-a716-446655440002",
"observed_at": "2026-08-01T09:30:00Z",
"requested_url": "https://example.com",
"successful_url": "https://www.example.com",
"content_fingerprint": "a3b1000000000000000000000000000000000000000000000000000000000000",
"byte_length": 48213,
"optimized_markdown": "# Example GmbH\n\nWir bauen ..."
}
403 with AUTHORIZATION_ERROR means the key lacks delivery:read. 404
means the company was not delivered to this search, or no website evidence
exists. Do not fall back to live pipeline company routes.
Incremental Sync¶
Store the latest successfully upserted delivered_at timestamp in your system.
On the next run, send it as delivered_since.
curl -sS \
"https://api.openprospect.io/api/v1/deliveries/${PROSPECT_SEARCH_ID}/companies?delivered_since=2026-02-25T12:00:00Z" \
-H "Authorization: Bearer ${OPENPROSPECT_API_KEY}"
delivered_since is a lower bound, not a snapshot cursor. Apply the same value
to every page request in one run, advance offset by the number of items
returned until has_more is false, and idempotently upsert repeated IDs.
Only after the complete run succeeds should you advance the stored checkpoint
to the greatest delivered_at value you successfully persisted.