API & Webhooks
Create leads via the API and push changes to other systems.
inDeal API: create and update leads via the interface
With the inDeal API you create leads from other systems, read them, keep them in sync and write activities such as notes or calls to the timeline. Everything you need lives under Settings -> API & Webhooks. Contents - Create a key - Basics - Create a lead - Read a lead - Update a lead - List leads - Activities - Fields - Duplicate leads - Error codes - Next Create a key 1. Open Settings -> API & Webhooks. 2. Click Create new key and pick a name that describes the connected system. 3. Copy the key right away. Copy the key now. It is shown only once and cannot be retrieved again later. The dialog for creating a key with the name CRM-Sync The one-time display of your new key with a copy button Afterwards the list only shows the last four characters. If you need the key again, revoke the old one with Revoke and create a new one. Up to 10 active keys are possible, so you can give each system its own and revoke them individually. The key list with the masked key CRM-Sync and the connection details below Basics | Item | Value | |---|---| | Base URL | https://app.indeal.ai/api/v1 | | Authentication | Header Authorization: Bearer indeal_... | | Format | JSON in request and response | | Limit | 600 requests per minute per key | Above the limit the API responds with status 429 and the header Retry-After: 60 - wait a minute in that case. Create a lead POST /leads creates a lead. At least one of email or linkedin_url (the contact's profile URL) is required. curl -X POST https://app.indeal.ai/api/v1/leads \ -H "Authorization: Bearer indeal_..." \ -H "Content-Type: application/json" \ -d '{ "full_name": "Julia Weber", "email": "julia.weber@muster.de", "company_name": "Muster GmbH", "role_title": "Managing Director" }' Response on success (status 201): { "status": "created", "lead": { "id": "0b0e...", "stage": "qualifying", "full_name": "Julia Weber", "email": "julia.weber@muster.de", "company_name": "Muster GmbH" } } If the lead already exists you get status 200 with "status": "duplicate" and the existing lead - see Duplicate leads. Read a lead GET /leads/{id} returns a single lead. curl https://app.indeal.ai/api/v1/leads/0b0e... \ -H "Authorization: Bearer indeal_..." Response (status 200): { "lead": { ... } }. An unknown id returns status 404. Update a lead PATCH /leads/{id} changes only the fields you send. null clears a field. stage moves the lead to another phase - the history in inDeal is written automatically. custom_data is merged, not replaced: null as a value deletes a key, all other keys stay. curl -X PATCH https://app.indeal.ai/api/v1/leads/0b0e... \ -H "Authorization: Bearer indeal_..." \ -H "Content-Type: application/json" \ -d '{ "stage": "meeting_scheduled", "next_step": "Prepare demo" }' Response (status 200): { "lead": { ... } }. If the change collides with an existing lead (email or profile URL already taken), you get status 409. List leads GET /leads returns leads page by page, sorted by last change. curl "https://app.indeal.ai/api/v1/leads?updated_since=2026-09-01T00:00:00Z&limit=100" \ -H "Authorization: Bearer indeal_..." | Parameter | Meaning | |---|---| | updated_since | only leads changed since this point in time (ISO 8601) | | limit | 1 to 200, default 100 | | cursor | next_cursor from the previous response | | order | asc (default, oldest change first) or desc (newest first) | Response: { "leads": [...], "next_cursor": "..." }. When next_cursor is null you have reached the end. The cursor remembers the sort direction, so you do not have to repeat order while paging. For ongoing sync: query regularly with updated_since and page with cursor. Find a lead by email or profile URL You know the email address or the profile URL and need the id? Pass it as a parameter. inDeal then searches exactly as when creating: upper and lower case do not matter, and for the profile URL neither do https://, www or a trailing slash. curl "https://app.indeal.ai/api/v1/leads?email=julia.weber@muster.de" \ -H "Authorization: Bearer indeal_..." | Parameter | Meaning | |---|---| | email | match on the email address | | linkedin_url | match on the profile URL | If you pass both, you get every lead that matches at least one of them. The response is { "leads": [...] } without next_cursor, since there can never be more than two matches. If nothing matches, the list is empty. updated_since and cursor are ignored for this search. An invalid email address or URL returns status 400 with the field name. Activities Every lead has a timeline. Via the API you add entries to it and read them. Create an activity POST /leads/{id}/activities adds a timeline entry to the lead. Writable types are note, call, email and meeting_scheduled. At least one of title or body is required. If title is missing, inDeal builds it from the first 80 characters of body. | Field | Required | Note | |---|---|---| | type | yes | note, call, email, meeting_scheduled | | title | no | max. 200 characters | | body | no | free text, max. 10000 characters, for example the call outcome | | channel | no | linkedin, email, meet, call | | occurred_at | no | point in time (ISO 8601), default now, not in the future | curl -X POST https://app.indeal.ai/api/v1/leads/0b0e.../activities \ -H "Authorization: Bearer indeal_..." \ -H "Content-Type: application/json" \ -d '{ "type": "call", "body": "Short call, demo next week", "channel": "call" }' Response on success (status 201): { "activity": { "id": "9f3a...", "type": "call", "title": "Short call, demo next week", "body": "Short call, demo next week", "quote": null, "channel": "call", "occurred_at": "2026-09-03T14:00:00+00:00", "created_at": "2026-09-03T14:00:01+00:00", "pinned": false, "edited_at": null } } The entry appears in the lead's timeline right away, and calls as well as emails count as touchpoints. The types reply (a reply from a campaign) and stage_change (a stage change) are written by inDeal itself; sending them returns status 400. An unknown lead id returns status 404. Read the timeline GET /leads/{id}/activities returns all entries of the lead, newest first, including the read-only types reply and stage_change. | Parameter | Meaning | |---|---| | type | one type only: note, call, email, meeting_scheduled, reply, stage_change | | limit | 1 to 200, default 50 | | cursor | next_cursor from the previous response | curl "https://app.indeal.ai/api/v1/leads/0b0e.../activities?type=reply&limit=50" \ -H "Authorization: Bearer indeal_..." Response: { "activities": [...], "next_cursor": "..." }. For replies from campaigns the text is in quote. edited_at shows that an entry was edited later. There is no update or delete via the API. If you want to be notified about new entries right away instead of polling the timeline, subscribe to the webhook event activity.created - see inDeal Webhooks. Fields Required rule: at least one of email / linkedin_url when creating. Everything else is optional. | Field | Type | Note | |---|---|---| | full_name | text | contact name | | role_title | text | position | | email | text | dedup anchor | | phone, mobile, company_phone | text | cleaned automatically | | linkedin_url | text | profile URL, dedup anchor | | company_name | text | company | | company_size | integer | employee count | | industry | text | industry | | company_website, company_linkedin | text | company links | | street, zip, city, country | text | address | | next_step | text | next step | | next_step_due | date | format YYYY-MM-DD | | est_value | number | estimated value | | description | text | description | | lead_source | text | origin | | sentiment | choice | positive, neutral, negative | | channel_direction | choice | inbound, outbound | | lost_reason | text | lost reason | | stage | choice | qualifying, schedule_meeting, meeting_scheduled, meeting_no_show, meeting_done, lead_lost | | custom_data | object | custom fields, see Custom fields | Every response also contains url, the link to the lead page in inDeal (read-only). Unknown fields are rejected - the error message names the field. Internal fields (such as enrichment status) are neither returned nor accepted. Custom fields You create custom fields in inDeal under Customizing the layout, separately for leads and deals. Their values are in the custom_data object, the key is the field's API name. Listing the fields curl "https://app.indeal.ai/api/v1/custom-fields?entity=lead" \ -H "Authorization: Bearer indeal_..." entity is required (lead or deal). The response contains all active fields in layout order, each with key (API name), label, type, options for dropdowns, currency_code for currency and the section. Hidden fields are not included. Value format per type | type | Value in custom_data | Example | |---|---|---| | text | text, max. 10,000 characters, trimmed | "Call back after the fair" | | url | link, http or https only; https:// is added if missing | "https://indeal.ai" | | number | number | 42 | | currency | number; the currency is in the field's currency_code | 5000 | | checkbox | true or false | true | | date | YYYY-MM-DD | "2026-10-15" | | datetime | ISO 8601 with offset | "2026-10-15T09:30:00+02:00" | | dropdown | the API name of an option, not its name | "gold" | | multiselect | list of option API names, duplicates are removed | ["fair", "referral"] | null deletes a value, so does an empty text or an empty list. Each request allows at most 50 keys in custom_data. Checks when writing POST and PATCH on leads and deals check custom_data against the fields of that module: - Unknown keys and keys of hidden fields are ignored and reported in warnings. That is not an error, the rest is saved. { "lead": { ... }, "warnings": [{ "key": "partner_id", "code": "unknown_key" }, { "key": "old", "code": "deleted_field" }] } - Invalid values for known fields return status 422 with validation_error and details. Nothing is written then, not even other fields from the same request. { "error": { "code": "validation_error", "message": "custom_data: invalid values.", "details": [ { "key": "plan", "code": "invalid_option" }, { "key": "budget", "code": "invalid_type" } ] } } Possible values for details.code: invalid_type, invalid_option, invalid_date, invalid_url, too_long, readonly_field. warnings is part of every write response, as an empty list if nothing was ignored. Duplicate leads When creating, inDeal first checks the profile URL, then the email address - both case-insensitively. If a matching lead already exists, NO new one is created: you get status 200 with "status": "duplicate" and the existing lead including its id. This makes the endpoint safe to repeat: sending the same request twice never creates two leads. To change the existing lead, use the id from the response with PATCH /leads/{id}. Error codes All errors share the same shape: { "error": { "code": "...", "message": "..." } }. The message is always in English and meant for humans, check the code in your code. { "error": { "code": "not_found", "message": "Lead not found." } } | Code | Status | Meaning | |---|---|---| | unauthorized | 401 | key missing, invalid or revoked | | rate_limited | 429 | limit exceeded, wait the seconds from Retry-After | | validation_error | 400 | invalid input, the message names the field | | validation_error | 422 | invalid values in custom_data, details names field and reason, nothing was written | | not_found | 404 | lead does not exist or is in the trash | | conflict | 409 | change collides with an existing lead | | blacklisted | 409 | person or company is on the blacklist, nothing was created | | internal | 500 | unexpected error, try again | | internal | 503 | blacklist check temporarily unavailable, nothing was created, try again later | Next - inDeal API: deals and reports - inDeal Webhooks: send lead and deal changes to other systems
inDeal API: deals and reports
With the inDeal API you create deals for your leads, move them through the stages to closing and fetch ready-made figures without paging through lists yourself. Key, base URL and basics are the same as for leads: inDeal API: create and update leads via the interface. Contents - What a deal is - Create a deal - Read and update a deal - List deals - Activities on a deal - Fields - Reports - The figures in detail - Next What a deal is A deal comes from a lead and keeps its origin in lead_id. It moves through the stages send_offer and order_received and ends in won or lost. Each lead has at most one open deal - the same rule as in the app, see From lead to deal and back. Create a deal POST /deals creates a deal. lead_id is required; the lead has to exist in your account, otherwise the API responds with 404. curl -X POST https://app.indeal.ai/api/v1/deals \ -H "Authorization: Bearer indeal_..." \ -H "Content-Type: application/json" \ -d '{ "lead_id": "0b0e...", "value": 12000, "confidence": 40, "expected_close_date": "2026-10-15" }' Response with status 201: { "status": "created", "deal": { "id": "7c1d...", "lead_id": "0b0e...", "stage": "send_offer", "value": 12000, "confidence": 40, "weighted_value": 4800, "expected_close_date": "2026-10-15", "won_at": null, "lost_at": null, "url": "https://app.indeal.ai/pipeline/7c1d...", "lead": { "id": "0b0e...", "full_name": "Julia Weber", "company_name": "Muster GmbH", "email": "julia.weber@muster.de", "url": "https://app.indeal.ai/leads/0b0e..." } } } Three things happen automatically: - If value is missing, the deal takes the lead's estimated value (est_value). If confidence is missing, it is 25. - The lead now counts as converted. Its stage stays unchanged. - If the lead already has a deal that is not in the trash, no second one is created. You get status 200 with "status": "duplicate" and the existing deal. So simply check status instead of looking it up first. Read and update a deal GET /deals/{id} returns { "deal": { ... } }. An unknown id or a deal from another account results in 404. PATCH /deals/{id} changes only the fields you send. null empties a field. curl -X PATCH https://app.indeal.ai/api/v1/deals/7c1d... \ -H "Authorization: Bearer indeal_..." \ -H "Content-Type: application/json" \ -d '{ "stage": "won", "value": 15000 }' - stage switches the stage. won sets won_at, lost sets lost_at to now. inDeal writes the history of stage changes itself. - With lost, lost_reason is worth adding - the reason shows in the app and in the webhook event deal.lost. - If you change next_step_due, a reminder set in the app for that date is dropped. List deals GET /deals returns deals page by page, sorted by last change. | Parameter | Meaning | |---|---| | stage | only this stage | | lead_id | only deals of this lead | | updated_since | ISO 8601, only deals changed since then | | order | asc (default, oldest first) or desc | | limit | 1 to 200, default 100 | | cursor | next_cursor from the previous response | Response: { "deals": [...], "next_cursor": "..." }. If next_cursor is null, that was the last page. For totals and counts you do not need this list - that is what Reports are for. Activities on a deal Deals have their own timeline. POST /deals/{id}/activities and GET /deals/{id}/activities work exactly like activities on a lead: the same types (note, call, email, meeting_scheduled), the same fields, the same paging. An entry on a deal belongs to the deal only, not additionally to the lead. Fields | Field | Type | Note | |---|---|---| | id, created_at, updated_at, stage_entered_at | read-only | | | lead_id | id | originating lead, required on create | | stage | choice | send_offer, order_received, won, lost | | value | number | deal volume | | confidence | integer 0 to 100 | probability in percent | | weighted_value | read-only | value times confidence divided by 100 | | expected_close_date | date YYYY-MM-DD | expected closing | | offer_sent_at, next_step_due | ISO 8601 | | | next_step, notes, lost_reason | text | lost_reason only on update | | won_at, lost_at | read-only | set when switching to won or lost | | url | read-only | link to the deal page in inDeal | | lead | read-only | id, full_name, company_name, email and url of the lead | | custom_data | object | custom fields for deals, same format and checks as for leads, see Custom fields; GET /custom-fields?entity=deal lists the fields | Not reachable via the API: the assignment to a team member, reminders and the trash. Reports GET /reports/summary returns ready-made figures for a period - leads, meetings, deals and conversion in one response. The figures cover the whole account, regardless of who a lead is assigned to. All times are UTC. | Parameter | Meaning | |---|---| | from | start, YYYY-MM-DD or ISO 8601. Default: the 1st of the current month | | to | end, YYYY-MM-DD (that day counts) or ISO 8601 (exclusive). Default: now | | sections | comma-separated from leads, meetings, deals, conversion. Default: all four | The period may be at most 366 days long and from has to be before to - otherwise the API responds with 400. curl "https://app.indeal.ai/api/v1/reports/summary?from=2026-09-01&to=2026-09-30§ions=deals,conversion" \ -H "Authorization: Bearer indeal_..." Response with status 200, here with all four blocks: { "period": { "from": "2026-09-01T00:00:00+00:00", "to": "2026-10-01T00:00:00+00:00" }, "leads": { "created": 12, "by_stage": [ { "stage": "qualifying", "count": 40, "est_value_sum": 380000 } ], "by_source_tool": [ { "source_tool": "api", "count": 12 } ], "by_lead_source": [ { "lead_source": "Webinar", "count": 7 } ] }, "meetings": { "scheduled": 5, "done": 3 }, "deals": { "created": 3, "by_stage": [ { "stage": "send_offer", "count": 4, "value_sum": 54000, "weighted_sum": 21600 } ], "pipeline_value": 54000, "pipeline_weighted": 21600, "won_count": 1, "won_value": 15000, "lost_count": 1, "avg_days_to_won": 12.5 }, "conversion": { "leads_created": 12, "meetings_scheduled": 5, "deals_created": 3, "deals_won": 1, "meeting_rate": 0.4167, "deal_rate": 0.6, "win_rate": 0.3333 } } Rates are numbers between 0 and 1 with four decimals. Leads and deals in the trash never count. The figures in detail The response holds two kinds of numbers: counters for the period (what happened between from and to) and snapshot figures of today (what the pipeline looks like right now). by_stage, pipeline_value and pipeline_weighted are snapshot figures, everything else refers to the period. | Figure | Meaning | |---|---| | period.from, period.to | the evaluated period, from inclusive, to exclusive | | leads.created | leads created in the period | | leads.by_stage | all current leads per stage with count and sum of the estimated value | | leads.by_source_tool | leads created in the period per source (for example api, import, extension) | | leads.by_lead_source | leads created in the period per origin from the lead source field, the 20 most frequent; empty means unknown | | meetings.scheduled | leads moved to meeting scheduled in the period | | meetings.done | leads moved to meeting done in the period | | deals.created | deals created in the period | | deals.by_stage | all current deals per stage with count, sum of values and weighted sum | | deals.pipeline_value | sum of the values of all open deals (send offer, order received), as of today | | deals.pipeline_weighted | the same sum, weighted by confidence | | deals.won_count, deals.won_value | deals won in the period, count and sum of values | | deals.lost_count | deals lost in the period | | deals.avg_days_to_won | average days from creating the deal to winning it, only for deals won in the period; null without won deals | | conversion.leads_created | same as leads.created | | conversion.meetings_scheduled | same as meetings.scheduled | | conversion.deals_created | same as deals.created | | conversion.deals_won | same as deals.won_count | | conversion.meeting_rate | meetings scheduled divided by leads created; null without leads | | conversion.deal_rate | deals created divided by meetings scheduled; null without meetings | | conversion.win_rate | deals won divided by deals created; null without deals | The rates in conversion use the counters of the same period. They are therefore not a true funnel of one cohort: a meeting in September can belong to a lead from August. Next - inDeal API: create and update leads via the interface - inDeal Webhooks: send lead and deal changes to other systems - MCP: Connect Claude, ChatGPT and Cursor
inDeal Webhooks: send lead and deal changes to other systems
With webhooks inDeal reports lead and deal changes and new activities to your systems on its own - instead of you polling regularly. Every change arrives as a POST to a URL of your choice. Contents - Create a webhook - Events - What arrives - Verify the signature - Delivery and retries - If your endpoint was disabled - Testing and resending - Rotating the secret - Zapier, Make and n8n - Next Create a webhook 1. Open Settings -> API & Webhooks and go to the Webhooks section. 2. Click Add webhook. Enter a name and the target URL - it must start with https:// and point to a publicly reachable server. Addresses on internal networks, localhost, non-standard ports or redirects are rejected. Your receiver has to accept POST requests there and must not redirect. 3. All nine events are preselected, four for leads and five for deals. If you only want some of them, open Advanced: send only specific events and untick the others. 4. After creating, your webhook is active and you see your webhook secret. If your receiver should verify that requests really come from inDeal, copy it now - it is shown only once. For n8n, Zapier or Make you usually do not need it. If you need it later after all, generate a new one from the list with Rotate secret, see Rotating the secret. The dialog for creating a webhook with name and target URL The one-time display of your webhook secret after creating Up to 5 webhooks are possible - for example one per connected system. Webhooks you created before the deal events existed do not receive them automatically. The list then shows New: deal events available. - click Edit and tick the events you need. Events | Event | When | |---|---| | lead.created | A new lead is created, no matter how: manually, via import, via the extension, via the API or from campaigns | | lead.updated | At least one lead field or a custom field changed | | lead.stage_changed | The stage changed | | activity.created | A new entry in the timeline of a lead or a deal: note, call, email, meeting or a reply from a campaign - whether added manually, via import, from the email sync or via the API | | deal.created | A new deal is created, in the app or via the API | | deal.updated | At least one deal field changed (value, confidence, closing date, next step, notes) or a custom field | | deal.stage_changed | The deal stage changed, to whichever stage | | deal.won | The deal was set to won - comes in addition to deal.stage_changed | | deal.lost | The deal was set to lost - comes in addition to deal.stage_changed, with the lost reason | A stage change only produces lead.stage_changed or deal.stage_changed, no additional lead.updated or deal.updated and no activity.created. Closing a deal produces two deliveries: deal.stage_changed plus deal.won or deal.lost. If you only care about the closing, select the last two and leave out deal.stage_changed. The event selection under Advanced with the four lead and five deal events in two columns What arrives Every delivery is a POST with a JSON body: { "event": "lead.updated", "occurred_at": "2026-09-03T10:15:00+00:00", "lead_id": "0b0e...", "workspace_id": "aa70...", "data": { "id": "0b0e...", "full_name": "Julia Weber", "stage": "qualifying", "company_name": "Muster GmbH", "custom_data": {} }, "changes": { "company_name": { "from": null, "to": "Muster GmbH" } } } - data contains the current state of the lead (the same fields as the API response). - changes differs per event: for lead.updated it holds only the changed fields with old and new value, for lead.stage_changed it holds { "stage": { "from": "qualifying", "to": "meeting_scheduled" } }, for lead.created and activity.created it is empty. - Deliveries from the test button additionally carry "test": true. For activity.created the object activity comes along - the same fields as when creating an activity via the API. data is the current state of the lead here as well: { "event": "activity.created", "occurred_at": "2026-09-03T14:00:00+00:00", "lead_id": "0b0e...", "workspace_id": "aa70...", "activity": { "id": "9f3a...", "type": "call", "title": "Short call, demo next week", "body": "Short call, demo next week", "quote": null, "channel": "call", "occurred_at": "2026-09-03T14:00:00+00:00", "created_at": "2026-09-03T14:00:01+00:00", "pinned": false, "edited_at": null }, "data": { "id": "0b0e...", "full_name": "Julia Weber", "stage": "qualifying", "company_name": "Muster GmbH", "custom_data": {} }, "changes": {} } type is one of note, call, email, meeting_scheduled or reply. For reply (a reply from a campaign) the reply text is in quote. The entity field says what the entry belongs to: lead or deal. For an entry on a deal, deal_id is set, lead_id is empty and data is the current state of the deal. Deal events look the same, just with deal_id and the deal in data - the same fields as the deal response of the API, including the lead it belongs to. The values of custom fields are in custom_data for leads and deals, the key is the field's API name. When a custom field changes, you get lead.updated or deal.updated. An example of a deal event: { "event": "deal.lost", "occurred_at": "2026-09-05T09:30:00+00:00", "deal_id": "7c1d...", "lead_id": "0b0e...", "workspace_id": "aa70...", "data": { "id": "7c1d...", "lead_id": "0b0e...", "stage": "lost", "value": 12000, "confidence": 40, "weighted_value": 0, "lost_reason": "Budget cut", "won_at": null, "lost_at": "2026-09-05T09:30:00+00:00", "lead": { "id": "0b0e...", "full_name": "Julia Weber", "company_name": "Muster GmbH", "email": "julia.weber@muster.de" } }, "changes": { "stage": { "from": "send_offer", "to": "lost" }, "lost_reason": { "from": null, "to": "Budget cut" } } } - For deal.updated, changes holds only the changed fields with old and new value. - For deal.stage_changed and deal.won it holds the stage change, for deal.lost additionally lost_reason. - For deal.created, changes is empty. These headers come along: | Header | Content | |---|---| | X-Indeal-Signature | sha256=... - the signature, see below | | X-Indeal-Timestamp | Unix seconds of sending | | X-Indeal-Event | the event type | | X-Indeal-Delivery | unique id of the delivery | Verify the signature The signature proves the request really comes from inDeal. The text timestamp.body is signed with your secret (HMAC-SHA256). Verify with a constant-time comparison and only accept timestamps at most 5 minutes old. Node.js: const { createHmac, timingSafeEqual } = require("node:crypto"); function verify(secret, req, rawBody) { const ts = Number(req.headers["x-indeal-timestamp"]); if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false; const expected = "sha256=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex"); const got = req.headers["x-indeal-signature"] ?? ""; const a = Buffer.from(expected); const b = Buffer.from(got); return a.length === b.length && timingSafeEqual(a, b); } Python: import hashlib, hmac, time def verify(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool: if abs(time.time() - int(timestamp)) > 300: return False mac = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256) return hmac.compare_digest("sha256=" + mac.hexdigest(), signature) Important: verify against the RAW request body, not against re-serialized JSON - otherwise the comparison fails. Delivery and retries Respond with a status between 200 and 299. Anything else, including no response within 10 seconds, counts as a failed attempt. - inDeal tries each delivery up to 8 times, with growing gaps: 1, 2, 4, 8, 16, 32, 64 minutes. - After the eighth failure the delivery is considered finally failed. You can see it in the Recent deliveries panel and push it again with Resend. - If 50 deliveries fail in a row, inDeal disables the webhook automatically, see If your endpoint was disabled. From 5 consecutive failures the list already shows a yellow hint. - Rarely a delivery can arrive twice. Use the X-Indeal-Delivery header to detect duplicates. If your endpoint was disabled If 50 deliveries fail in a row, inDeal switches the webhook off. From then on no events are sent to this address, not even new ones. - You hear about it right away. All admins of the account receive an email with the name, target URL, the last error in plain words and the time. In the webhook list the webhook shows in red as Disabled with the same reason. - Fix the cause. The most common case: the receiver responds with 404 or 405 because the URL is no longer valid or does not accept POST. With n8n this is typically a test URL with /webhook-test/ in the path. It only works while the workflow is waiting for a test event. For continuous operation you need the production URL of the activated workflow, which contains /webhook/ without -test. - Re-enable. Click Re-enable in the list. The failure counter starts at zero and new events are delivered again. Deliveries that finally failed in the meantime can be pushed again with Resend in the Recent deliveries panel. Testing and resending - Send test creates a sample delivery of type lead.created with "test": true in the body. It runs through the same path as real deliveries and reaches your receiver within a minute. This lets you check reachability and signature without creating a real lead. Up to 5 tests per minute and webhook are possible, and only while the webhook is active. - The Recent deliveries panel shows status, event, attempts, HTTP code and time per delivery. Resend immediately pushes an undelivered or finally failed delivery again. Your webhook row with expanded deliveries: two successful and one finally failed with an error text Rotating the secret The secret is shown only once. If you lost it, or want to replace it on a schedule, you do not need to recreate the webhook: 1. Click Rotate secret in the webhook row and confirm. 2. inDeal shows the new secret exactly once. Copy it and store it with your receiver. The old secret is invalid from the moment you click. Deliveries sent after that are signed with the new secret - if your receiver still holds the old one, its signature check fails until you replace it there. Name, URL, events and the delivery history stay unchanged. If your receiver does not verify the signature (for example Zapier, Make or n8n without a code step), it will not notice the rotation. Zapier, Make and n8n All three tools offer webhook triggers that generate a URL: 1. Create a webhook trigger there (Zapier: "Catch Hook", Make and n8n: a webhook node) and copy the generated URL. 2. Enter that URL as the target URL in inDeal. Best keep all events selected and filter on the event field in the tool. 3. Click Send test and build the rest of your flow from the received sample. Signature verification is optional with these tools - the URLs are long and random, you usually do not need the secret. If you still want to verify, use a code step with the sample above. Next - inDeal API: create and update leads via the interface