Webhooks

Receive search results via signed webhook delivery.

When a search completes (or fails), Soundstripe sends a POST request to the callback_url you provided when creating the search. The webhook payload contains the full search results in the same JSON:API format as the create a search endpoint.

Webhook headers

HeaderDescription
Content-Typeapplication/vnd.api+json
X-Soundstripe-SignatureHMAC-SHA256 signature for verifying the webhook authenticity.
X-Soundstripe-TimestampUnix timestamp (seconds) of when the webhook was signed.

Webhook payload

The payload follows the same format as a completed search response: a JSON:API resource with status: "completed", the original query, context, and asset_ids, and included songs with their audio_files and artists resources.

{
  "data": {
    "id": "fa9b5222768b...",
    "type": "supe_searches",
    "attributes": {
      "status": "completed",
      "query": "upbeat indie rock for a travel vlog",
      "context": "Summer road trip montage with golden-hour visuals",
      "asset_ids": []
    },
    "relationships": {
      "songs": {
        "data": [
          { "id": "12556", "type": "songs" }
        ]
      }
    },
    "links": {
      "self": "https://api.soundstripe.com/v1/supe/search/fa9b5222768b..."
    }
  },
  "included": [
    {
      "id": "12556",
      "type": "songs",
      "attributes": {
        "title": "After The Storm",
        "description": "A low energy, classical song that is best described as sad and reflective.",
        "energy": "low",
        "explicit": false,
        "lyrics": "",
        "bpm": 90,
        "key": { "name": "F", "mode": "minor" },
        "tags": {
          "genre": ["Classical", "Underscore", "Soundtrack / Cinematic"],
          "mood": ["Calm", "Reflective", "Sad"],
          "instrument": ["Piano", "Strings"],
          "characteristic": ["Atmospheric", "Beautiful", "Minimal"]
        },
        "playlist_ids": [4012, 4097],
        "stems": "https://cdn.soundstripe.com/.../Moments_AfterTheStorm.zip?key=YOUR_API_KEY&search_id=fa9b5222768b...&token=...",
        "has_vocal_version": false,
        "has_instrumental_version": true
      },
      "relationships": {
        "artists": { "data": [{ "id": "506", "type": "artists" }] },
        "audio_files": { "data": [{ "id": "47652", "type": "audio_files" }] }
      }
    }
  ]
}

Verifying the webhook signature

Every webhook includes an X-Soundstripe-Signature header containing an HMAC-SHA256 signature. Verify the signature to confirm the webhook came from Soundstripe and has not been tampered with.

The signature is computed over the string {timestamp}.{payload}, where timestamp is the value of the X-Soundstripe-Timestamp header and payload is the raw request body. Your API key is used as the HMAC secret.

Ruby:

timestamp = request.headers["X-Soundstripe-Timestamp"]
signed_content = "#{timestamp}.#{request.raw_post}"
expected = OpenSSL::HMAC.hexdigest("SHA256", api_key, signed_content)
verified = ActiveSupport::SecurityUtils.secure_compare(
  expected,
  request.headers["X-Soundstripe-Signature"]
)

Python:

import hmac
import hashlib

timestamp = request.headers["X-Soundstripe-Timestamp"]
signed_content = f"{timestamp}.{request.body.decode()}"
expected = hmac.new(
    api_key.encode(),
    signed_content.encode(),
    hashlib.sha256,
).hexdigest()
verified = hmac.compare_digest(expected, request.headers["X-Soundstripe-Signature"])

Node.js:

const crypto = require("crypto");

const timestamp = request.headers["x-soundstripe-timestamp"];
const signedContent = `${timestamp}.${request.rawBody}`;
const expected = crypto
  .createHmac("sha256", apiKey)
  .update(signedContent)
  .digest("hex");
const verified = crypto.timingSafeEqual(
  Buffer.from(expected),
  Buffer.from(request.headers["x-soundstripe-signature"])
);
🚧

Use constant-time comparison

Always use a constant-time comparison function (like timingSafeEqual, secure_compare, or compare_digest) to prevent timing attacks.

Failed searches

If a search fails, a webhook is still delivered with status: "failed" and an empty songs array. Your webhook handler should check the status field before processing results.

{
  "data": {
    "id": "fa9b5222768b...",
    "type": "supe_searches",
    "attributes": {
      "status": "failed",
      "query": "upbeat indie rock for a travel vlog",
      "context": "",
      "asset_ids": []
    },
    "relationships": {
      "songs": { "data": [] }
    },
    "links": {
      "self": "https://api.soundstripe.com/v1/supe/search/fa9b5222768b..."
    }
  }
}

The webhook payload deliberately omits a top-level links.self. The webhook is a server-initiated POST and has no inbound request URL to reflect; use data.links.self to refetch the search via the retrieve a search endpoint.

A failed search typically reflects a vague or contradictory request that the agent could not converge on, a transient upstream timeout, or an asset that did not finish processing in time. Customers can safely retry a failed search by submitting a new request.

Webhook payload size

Webhook bodies are JSON only, with no binary or base64-encoded audio. Size scales with the number of songs returned and their associated audio_files and artists rows. Observed size distribution from production load testing:

PercentileSize
p50103 KB
Mean162 KB
p95464 KB
p99725 KB
Max observed1,049 KB

Receiver sizing recommendation. Configure your webhook receiver to accept request bodies of at least 2 MB, ideally 10 MB, so high-result searches are not rejected by your reverse proxy or framework. Common defaults that need attention:

  • Nginx: client_max_body_size 10m;
  • Cloudflare Workers or similar edge platforms: confirm your platform's request-body limit.
  • AWS API Gateway: default payload limit is 10 MB.
  • Node Express body-parsers: express.json({ limit: "10mb" }).

If your receiver rejects oversize bodies with a 4xx (for example HTTP 413 Payload Too Large), the delivery is treated as a failure and retried per the retry policy below.

Retry policy

Failed webhook deliveries are retried up to 3 times with exponential backoff. If all retries are exhausted, the search is marked as delivery_failed and the results can still be retrieved using the retrieve a search endpoint. Your webhook handler should return a 2xx status code to acknowledge receipt.

Idempotency

Webhooks may be delivered more than once due to retries or race conditions. Use the data.id field in the payload to deduplicate. Your webhook handler should be idempotent.