SocialHz Engineering · 1 November 2026 · Requested by a customer

Background search: async=true, progress and a signed webhook

GET /api/v1/search?async=true · GET /api/v1/search/{id} · GET /api/v1/search/{id}/posts · DELETE /api/v1/search/{id}

What shipped

Add async=true to a search and it answers straight away with 202 Accepted and a search_id. We fetch every page on our side with no time limit, and you read the posts when it's done.

Why

A customer's weekly report covered many keywords over a date range. A single request runs for about two minutes, which wasn't enough for a search that size. And on Instagram, the platform's own date filter was leaving most posts in the range out.

1. Start

plan.max_credits is held up front, so you know the most the search can cost before it runs. Pages a keyword didn't need are given back as it finishes. pages is per keyword, 1 to 10, and 3 if you don't send it.

GET /api/v1/search?platform=instagram&query=Brightleaf or Northwind
    &start_date=2026-10-01&end_date=2026-10-31&pages=5
    &async=true&webhook_url=https://example.com/hooks/socialhz

HTTP/1.1 202 Accepted
Location: /api/v1/search/srch_7f3a9c21d4e8b6a0

{ "search_id": "srch_7f3a9c21d4e8b6a0", "status": "queued",
  "plan": { "terms": ["Brightleaf", "Northwind"], "pages_per_term": 5,
            "max_pages": 10, "credits_per_page": 1, "max_credits": 10 },
  "credits": { "held": 10, "given_back": 0 },
  "webhook_secret": "whsec_…" }

2. Follow

GET /api/v1/search/srch_7f3a9c21d4e8b6a0

{ "status": "running",
  "progress": { "pages_done": 4, "pages_planned": 10, "percent": 40, "posts_found": 37 },
  "terms": [
    { "term": "Brightleaf", "status": "running", "pages_done": 3, "posts": 30 },
    { "term": "Northwind",  "status": "done", "pages_done": 1, "posts": 7, "ended": "no_more_results" } ] }

3. The webhook

When it finishes we POST search.finished to webhook_url. To check it came from us, v1 is the hex HMAC-SHA256 of <t>.<body>, keyed with the webhook_secret from step 1. Answer with any 2xx. Otherwise we try again after 30 seconds, then 2, 10 and 30 minutes. The address must be https and public, and redirects aren't followed.

X-SocialHz-Event: search.finished
X-SocialHz-Signature: t=1793522834,v1=5f2c…

4. Read

GET /api/v1/search/{id}/posts?page=1&page_size=1000 (100 if not sent; term=Brightleaf for one keyword). total_posts and total_pages are exact, the order is fixed, and every post appears once with matched_terms. Read pages in any order, as often as you like: reading is free. Before the search is done, this answers 409 search_running.

Dates on Instagram

In a background search, Instagram dates are applied to each page's posts by publish time, rather than passed to Instagram's own date filter. In our tests that filter was leaving most of the posts in a range out. totals.outside_dates_left_out counts the posts dropped for falling outside your dates.

Cancel and retention

DELETE /api/v1/search/{id} stops a search. Pages already fetched are charged and kept to read, and the rest of the hold is given back. Results are readable for 14 days, then archived but still readable from the same URL with archived: true, until 180 days.

Switched on per workspace. Ask us and we'll turn it on for yours.

Request a feature

Tell us what you need on the updates page. The engineering team reads every request.

Background search reference · All engineering updates