# Create Webhook `POST https://api.getprospect.com/v2/webhooks` Subscribes an https URL on a public host to event types. Answers 201 with the signing `secret`, shown only here; subscribing the same URL to the same events again, with the same credential, answers 200 with that webhook and its secret, so a retried create never makes a second one. At most 25 active webhooks per workspace. Free: no credits. > **How are credits spent?** > > Webhooks are free. Creating one, the events it receives and test events > consume no credits, and an event never reveals a field that a > [People Search](https://getprospect.com/api-docs/people-search.md) by your workspace would mask. ## Event types A webhook receives a JSON `POST` at its `url` for each event type listed in its `events`: - **`person.saved`** - a person entered your workspace's saved people, from the app, the browser extension, a CSV enrichment or [Save People](https://getprospect.com/api-docs/people-save.md). Sent once per person; again only after the person was removed and saved anew. - **`person.email_found`** - a saved person has a valid email your workspace owns, at save time or later when an email is found. - **`company.saved`** - a company entered your workspace's saved companies ([Save Companies](https://getprospect.com/api-docs/company-save.md)). - **`webhook.test`** - sent only by [Send Test Event](https://getprospect.com/api-docs/webhooks-test.md). Only changes after the webhook was created are sent. Nothing is replayed from before, so subscribing never floods your receiver with history. ## Event body ```json { "id": "evt_6702a1b2c3d4e5f607182931", "type": "person.saved", "api_version": "2026-10", "created_at": "2026-10-08T12:00:40.000Z", "webhook_id": "6702a1b2c3d4e5f607182930", "data": { "person": { "id": "64f1a2b3c4d5e6f7a8b9c0d1", "...": "..." }, "saved_at": "2026-10-08T12:00:00.000Z", "list_ids": ["64f1a2b3c4d5e6f7a8b9c0f1"] } } ``` For person events `data.person` is the [Person object](https://getprospect.com/api-docs/person-object.md) as [People Search](https://getprospect.com/api-docs/people-search.md) returns it for your workspace. For `company.saved` it is `{ "company", "saved_at", "list_ids" }` with the [Company object](https://getprospect.com/api-docs/company-object.md). Deliveries are at least once and `id` stays the same across retries, so dedupe on it. A change of shape ships as a new `api_version`; a webhook keeps the version it was created with. ## Verify the signature Every delivery carries these headers: - `X-GetProspect-Event-Id` and `X-GetProspect-Event-Type` - `User-Agent: GetProspect-Webhooks/1.0` - `X-GetProspect-Signature: t=,v1=` `v1` is an HMAC-SHA256 over `.` with the webhook `secret`. The `secret` is in the answer of this endpoint only, so store it when you create the webhook. Check the signature on the raw body, before parsing it, and refuse an old `t`: ```js const crypto = require("crypto"); function isValid(header, rawBody, secret) { const [t, v1] = header.split(",").map((part) => part.split("=")[1]); const expected = crypto .createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex"); return ( v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex")) && Math.abs(Date.now() / 1000 - Number(t)) < 300 ); } ``` ## Delivery and retries Answer `2xx` within 10 seconds. Redirects are not followed. A transport error, `408`, `429` or `5xx` is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours; after that the event is `failed`. Any other answer is final. A retry is signed again with a new `t`. A webhook is turned off (`status: disabled`) when your receiver answers `410`, when deliveries did nothing but fail for 3 days, after 20 timeouts in a row, or when the member or API key that created it is gone. Once your receiver is fixed, re-activate it with [Update Webhook](https://getprospect.com/api-docs/webhooks-update.md); a webhook whose creator is gone cannot be re-activated, so create a new one. See what happened to each event in [List Webhook Deliveries](https://getprospect.com/api-docs/webhooks-deliveries.md). ## Limits The `url` must be `https` on a public host. A workspace can have 25 active webhooks. Each webhook has one delivery in flight at a time, and a workspace gets at most 10 deliveries per second. Creating a webhook with the same `url` and `events` as one of your active webhooks returns that webhook, so a retried request never creates a duplicate. ## Authentication Pass your API key in the `x-api-key` header. ## Body parameters - `url` (string, required, max length: 2048): Where events are POSTed: an https URL on a public host. Example: `{"url":"https://hooks.zapier.com/hooks/standard/1/abc/","events":["person.saved"]}` - `events` (enum[], required): A webhook event type: `person.saved` (a person entered the saved set), `person.email_found` (a saved person has a valid email the workspace owns), `company.saved` (a company entered the saved set). A webhook takes 1-10, no repeats. Allowed values: `person.saved`, `person.email_found`, `company.saved`. Example: `{"url":"https://hooks.zapier.com/hooks/standard/1/abc/","events":["person.saved"]}` - `description` (string, optional, max length: 200): A note for people reading the webhook list. - `api_version` (enum, optional): The payload version of the events (`2026-10`); defaults to the current one. Allowed values: `2026-10`. ## Example request ```bash curl --request POST \ --url https://api.getprospect.com/v2/webhooks \ --header 'Content-Type: application/json' \ --header 'x-api-key: YOUR_API_KEY' \ --data ' { "url": "https://hooks.zapier.com/hooks/standard/1/abc/", "events": [ "person.saved" ], "description": "New saved people to Zapier" } ' ``` ## Response 200 This credential already has an active webhook for this URL and these events: that one, with `secret`. ```json { "success": true, "data": { "id": "6702a1b2c3d4e5f607182930", "url": "https://hooks.zapier.com/hooks/standard/1/abc/", "events": [ "person.saved" ], "status": "active", "disabled_reason": null, "api_version": "2026-10", "description": "New saved people to Zapier", "created_by_user_id": "64a1b2c3d4e5f60718293a4b", "api_key_id": null, "last_delivery": null, "created_at": "2026-10-06T09:00:00.000Z", "updated_at": "2026-10-08T11:58:02.000Z", "secret": "whsec_4f9a2c7e1b8d4a6f9e0c3b5a7d2e8f1c" }, "metadata": { "timestamp": "2026-10-08T12:00:00.000Z" } } ``` ## Response 201 Created; `secret` is shown only on this answer and the 200 one. ```json { "success": true, "data": { "id": "6702a1b2c3d4e5f607182930", "url": "https://hooks.zapier.com/hooks/standard/1/abc/", "events": [ "person.saved" ], "status": "active", "disabled_reason": null, "api_version": "2026-10", "description": "New saved people to Zapier", "created_by_user_id": "64a1b2c3d4e5f60718293a4b", "api_key_id": null, "last_delivery": null, "created_at": "2026-10-06T09:00:00.000Z", "updated_at": "2026-10-08T11:58:02.000Z", "secret": "whsec_4f9a2c7e1b8d4a6f9e0c3b5a7d2e8f1c" }, "metadata": { "timestamp": "2026-10-08T12:00:00.000Z" } } ``` ## Response 400 Invalid body (`VALIDATION_ERROR`): `invalid_format` (not https, credentials in the URL, or a private host), `value_not_allowed` (unknown event type), `out_of_range` (1-10 events, or already 25 active webhooks), `missing_field`, `wrong_type`, `unknown_field`. ```json { "success": false, "data": null, "metadata": { "timestamp": "2026-10-08T12:00:00.000Z" }, "errors": [ { "name": "VALIDATION_ERROR", "message": "events: \"person.created\" is not an accepted value. Accepted: person.saved, person.email_found, company.saved", "payload": { "field": "events", "code": "value_not_allowed", "received_value": [ "person.created" ], "received_type": "object", "invalid_values": [ "person.created" ], "allowed_values": [ "person.saved", "person.email_found", "company.saved" ] } } ] } ``` ## Response 401 Missing, unknown or inactive credentials (`UNAUTHORIZED`). ## Response 500 Unexpected server error.