# Receiving leads

A lead intake endpoint is a private URL that turns a form submission or an automation step into a lead in banta. No API key and no code are needed: paste the URL into your form plugin or automation platform, and every submission becomes a lead.

Create one endpoint per source - for example "Website contact form" or "Facebook via Zapier" - in banta under **Settings → Connections & technology → Webhooks → Receiving leads**. Each endpoint has its own defaults: lead source, status, assignee, tags and duplicate handling. An organization can have up to 20 endpoints.

## The URL

| URL | When to use it |
|---|---|
| `https://app.banta.co.il/hooks/leads/<token>` | Recommended |
| `https://db.banta.co.il/functions/v1/lead-intake/<token>` | Required when the endpoint is restricted to IP addresses |

- The token (`blh_...`) is shown **only once**, when the endpoint is created. Treat the URL like a password. If it leaks, rotate the token in banta; the old URL stops working at once.
- Send `POST` requests from a server or an automation platform only. The endpoint does not accept requests from a browser (CORS) - a form that posts directly from a web page would expose the token. For a form embedded in your site, use banta public forms instead.

## Accepted requests

- **JSON** - an object, or an array with exactly one object. Nested objects are supported.
- **`application/x-www-form-urlencoded`** - including bracket keys such as `fields[name][value]`.
- **`multipart/form-data`** - text fields only; files are ignored.

One lead per request, up to 64 KB.

## Security

| Layer | Details |
|---|---|
| Secret token | Part of the URL. Always on. |
| IP allowlist | Optional. Single addresses or CIDR ranges, IPv4 and IPv6. Works only with the `db.banta.co.il` URL - the `https://app.banta.co.il` address goes through a proxy that hides the sender's address. |
| Signature | Optional. Every request must be signed with HMAC-SHA256 - see below. |
| Honeypot | Optional. Name a hidden form field, or use `_hp` or `honeypot`. A submission where it is filled is answered with `200` and discarded. |
| Rate limits | 300 requests per 10 minutes per endpoint. |

### Signing requests

When signing is enabled, banta shows a signing secret once. Every request must carry two headers:

- `X-Banta-Timestamp` - the current Unix time in seconds.
- `X-Banta-Signature` - `sha256=` followed by the lowercase hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the whole signing secret.

Requests whose timestamp is more than 5 minutes from the server clock are rejected. Sign the exact bytes you send. This is the same scheme banta uses for [outgoing webhooks](https://docs.banta.co.il/guides/webhooks.md).

**Node.js 18+**

```js
import crypto from 'node:crypto'

const url = 'https://app.banta.co.il/hooks/leads/blh_...'
const body = JSON.stringify({ first_name: 'Noa', last_name: 'Bar', phone: '0541234567', email: 'noa@example.com' })
const timestamp = Math.floor(Date.now() / 1000).toString()
const signature = crypto
  .createHmac('sha256', process.env.BANTA_SIGNING_SECRET)
  .update(`${timestamp}.${body}`)
  .digest('hex')

const res = await fetch(url, {
  method: 'POST',
  body,
  headers: {
    'Content-Type': 'application/json',
    'X-Banta-Timestamp': timestamp,
    'X-Banta-Signature': `sha256=${signature}`,
    'Idempotency-Key': 'crm-lead-12345',
  },
})
console.log(res.status, await res.json())
```

**Python**

```python
import hashlib, hmac, json, os, time
import requests

url = "https://app.banta.co.il/hooks/leads/blh_..."
body = json.dumps({"first_name": "Noa", "phone": "0541234567"}, ensure_ascii=False)
timestamp = str(int(time.time()))
signature = hmac.new(
    os.environ["BANTA_SIGNING_SECRET"].encode(),
    f"{timestamp}.{body}".encode(),
    hashlib.sha256,
).hexdigest()

r = requests.post(url, data=body.encode("utf-8"), headers={
    "Content-Type": "application/json",
    "X-Banta-Timestamp": timestamp,
    "X-Banta-Signature": f"sha256={signature}",
})
print(r.status_code, r.json())
```

**PHP / WordPress**

```php
<?php
// Define BANTA_SIGNING_SECRET in wp-config.php
$body      = wp_json_encode(['first_name' => 'Noa', 'phone' => '0541234567']);
$timestamp = (string) time();
$signature = hash_hmac('sha256', $timestamp . '.' . $body, BANTA_SIGNING_SECRET);

$response = wp_remote_post('https://app.banta.co.il/hooks/leads/blh_...', [
    'headers' => [
        'Content-Type'      => 'application/json',
        'X-Banta-Timestamp' => $timestamp,
        'X-Banta-Signature' => 'sha256=' . $signature,
    ],
    'body'    => $body,
    'timeout' => 10,
]);
```

To check your implementation, this input must produce this signature:

| | |
|---|---|
| Secret | `blhsec_0123456789abcdef0123456789abcdef0123456789abcdef` |
| Timestamp | `1760443200` |
| Body | `{"first_name":"Noa","phone":"0541234567"}` |
| Signature | `sha256=36423b4bb8b14256ffc3bf36d62c27f96fe90accc76eef115d5fbeafb3694111` |

Form plugins cannot sign requests. For them, rely on the secret URL, and add an IP allowlist if your server has a fixed address.

## Step by step

### Elementor Pro

1. Edit the form, and under **Actions After Submit** add **Webhook**.
2. In the **Webhook** section, paste the endpoint URL.
3. **Advanced Data** can be on or off - both formats are supported.
4. Update the page and send a test submission.

Elementor field IDs such as `name`, `email` and `message`, and field labels such as "Phone" or "טלפון", are recognized automatically. A field with a generated ID (`field_3f2a1b`) is recognized by its label when the label is one of the built-in names; otherwise map it by ID or by label. banta answers `200` on success, as Elementor requires, well within its 5-second timeout.

### Contact Form 7

Contact Form 7 needs a webhook add-on, for example "CF7 to Webhook".

1. Edit the form and open the add-on's **Webhook** tab.
2. Enable it, paste the endpoint URL and save.

The default fields `your-name`, `your-email`, `your-tel` and `your-message` are recognized. Other fields are kept in the lead's details note unless you map them.

### WPForms

Requires the WPForms **Webhooks** add-on.

1. In the form builder, open **Settings → Webhooks** and enable webhooks.
2. Add a webhook with the endpoint URL, method `POST` and format `JSON`.
3. In **Request Body**, add a row per field: the key is the banta field name (`first_name`, `last_name`, `phone`, `email`, `notes`) and the value is the form field.

### Gravity Forms

Requires the Gravity Forms **Webhooks Add-On**.

1. Open **Form Settings → Webhooks → Add New**.
2. Set the endpoint URL, method `POST` and format `JSON`.
3. For the request body, choose **Select Fields** with banta field names as keys, or **All Fields**.

With **All Fields**, Gravity sends numeric input IDs - for example `1.3` (first name), `1.6` (last name) and `2` (email). Map them in the endpoint's field mapping. Gravity's entry metadata (entry ID, IP address, user agent, dates) is ignored automatically.

### Zapier

1. Add the action **Webhooks by Zapier → POST**.
2. **URL**: the endpoint URL. **Payload Type**: `json`.
3. In **Data**, use banta field names as keys (`first_name`, `phone`, `email`, `utm_source`, ...).

### Make

1. Add the module **HTTP → Make a request**.
2. **URL**: the endpoint URL. **Method**: `POST`.
3. **Body type**: `Raw`. **Content type**: `JSON (application/json)`. **Request content**: a JSON object with banta field names.

### Facebook Lead Ads (through Zapier or Make)

1. Trigger: **Facebook Lead Ads → New Lead**. Action: **Webhooks by Zapier → POST** (or the Make HTTP module).
2. Map **Full Name**, **Phone Number** and **Email** to `full_name`, `phone` and `email` - or send the whole lead.

Facebook's `field_data` array and `campaign_name` (stored as `utm_campaign`) are recognized. Ad, ad set and form names are kept in the lead's details note. Map custom questions in the field mapping - for example `האם_יש_לך_נכס?` to `has_property`.

## Field mapping

Each incoming field is matched in this order:

1. The endpoint's field mapping, by field key or by label.
2. The built-in names in the table below. Matching ignores letter case, the difference between spaces, hyphens and underscores, and a trailing `*` or `:`. In nested JSON, the last part of the path is matched.
3. Technical fields of the form platforms (for example Elementor form metadata, Gravity entry data, captcha tokens and keys that start with `_`) are dropped.
4. Anything else is kept in a details note on the lead, so nothing the visitor typed is lost.

| banta field | Recognized names |
|---|---|
| `first_name` | first_name, firstname, first name, fname, first, given_name, שם פרטי, פרטי |
| `last_name` | last_name, lastname, last name, lname, last, surname, family_name, שם משפחה, משפחה |
| `full_name` | name, full_name, fullname, your-name, contact_name, שם, שם מלא, שם ושם משפחה, שם הלקוח |
| `phone` | phone, tel, mobile, telephone, phone_number, phonenumber, your-tel, your-phone, cell, cellphone, mobile_phone, whatsapp, טלפון, נייד, טלפון נייד, מספר טלפון, מספר נייד, פלאפון, סלולרי |
| `email` | email, e-mail, mail, email_address, your-email, אימייל, דוא"ל, דואל, מייל, דואר אלקטרוני, כתובת מייל |
| `notes` | message, notes, note, comments, comment, your-message, details, inquiry, enquiry, הודעה, הערות, הערה, פרטים, תוכן ההודעה, תוכן הפנייה, פנייה |
| `source` | source, lead_source, how_did_you_hear, מקור, מקור הגעה, מקור הליד, איך שמעת עלינו, איך הגעת אלינו |
| `mortgage_purpose` | mortgage_purpose, purpose, loan_purpose, מטרת המשכנתא, מטרת משכנתא, מטרת ההלוואה, מטרה |
| `required_amount` | required_amount, amount, loan_amount, mortgage_amount, סכום, סכום משכנתא, סכום המשכנתא, סכום מבוקש, סכום הלוואה, סכום ההלוואה |
| `total_income` | total_income, income, monthly_income, הכנסה, הכנסה חודשית, הכנסות, סך הכנסות |
| `has_property` | has_property, own_property, owns_property, יש נכס, בעל נכס, האם יש נכס, בעלות על נכס |
| `utm_source` ... `utm_content` | utm_source, utm_medium, utm_campaign (also campaign_name), utm_term, utm_content |
| `referrer_url` | referrer, referer, referrer_url, http_referer, document_referrer |
| `landing_path` | landing_path, landing_page, page_url, source_url, form_url, page_location |

Mapping targets: `first_name`, `last_name`, `full_name`, `phone`, `email`, `notes`, `source`, `source_id`, `mortgage_purpose`, `required_amount`, `total_income`, `has_property`, `next_follow_up_at`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`, `referrer_url`, `landing_path`, `custom_fields.<field_name>` and `ignore`.

- A full name is split into a first name (the first word) and a last name (the rest).
- A phone number or an email address is required. A lead without a name is named after the endpoint.
- Phone numbers are normalized; Israel is the default country.
- A lead source named in the request overrides the endpoint's default source.
- The status and the assignee always come from the endpoint's settings.
- The endpoint's event log shows the fields of recent submissions and how each one was mapped - the easiest way to build a mapping is to send one test submission and map from the log.

## Duplicates and repeated submissions

Each endpoint chooses how to handle a lead with the same phone number or email address as an existing lead:

| Mode | Behavior |
|---|---|
| `flag` (default) | The lead is created and marked as a possible duplicate. The response includes `duplicate_of`. |
| `reuse` | No new lead is created. The existing lead is returned, and the new submission is added to it as a note. |
| `none` | No duplicate check. |

Repeated deliveries of the same submission never create a second lead:

- An identical submission within 10 minutes returns the original result with the header `Idempotent-Replayed: true`.
- So does a request that repeats an idempotency key - the `Idempotency-Key` header, or `X-Idempotency-Key`, `webhook-id` or `X-Request-Id`.
- To create a second test lead, change any field.

## Testing with curl

**JSON**

```bash
curl -i -X POST "https://app.banta.co.il/hooks/leads/blh_..." \
  -H "Content-Type: application/json" \
  -d '{"full_name":"Test Lead","phone":"050-000-0000","email":"test@example.com","notes":"Sent with curl"}'
```

**A form post with Hebrew field names**

```bash
curl -i -X POST "https://app.banta.co.il/hooks/leads/blh_..." \
  --data-urlencode "שם מלא=ליד בדיקה" \
  --data-urlencode "טלפון=050-000-0000"
```

**Signed**

```bash
URL="https://app.banta.co.il/hooks/leads/blh_..."
BODY='{"full_name":"Test Lead","phone":"050-000-0000"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$BANTA_SIGNING_SECRET" | sed 's/^.* //')
curl -i -X POST "$URL" \
  -H "Content-Type: application/json" \
  -H "X-Banta-Timestamp: $TS" \
  -H "X-Banta-Signature: sha256=$SIG" \
  -d "$BODY"
```

Every request - accepted or rejected - appears in the endpoint's event log in banta for 30 days, with the reason for any rejection.

## Responses

| Status | Meaning |
|---|---|
| `200` | `{"ok": true, "lead_id": "...", "created": true}`. `created: false` means an existing lead was reused. `duplicate_of: {id, lead_number}` appears when a possible duplicate was found. A honeypot submission returns `{"ok": true}` only. |
| `400` | The body is invalid, or it has neither a phone number nor an email address: `{"ok": false, "error": "...", "details": "..."}` |
| `401` | The signature is missing or invalid |
| `403` | The sender's IP address is not allowed, or the organization has no active subscription |
| `404` | The endpoint does not exist or is disabled |
| `405` | A method other than `POST` |
| `409` | An identical request is still being processed - retry in a few seconds |
| `413` | The body is larger than 64 KB |
| `429` | Too many requests - see the `Retry-After` header |
| `500` | A temporary error - retry later |

Every success returns `200`, including a new lead, because some form plugins (Elementor Pro among them) treat any other status as an error.

## Limits

- 20 endpoints per organization.
- 300 requests per 10 minutes per endpoint.
- 64 KB and one lead per request.
- The first 200 fields are read, and values are cut at 5,000 characters.
- Signature timestamps must be within 5 minutes of the server clock.
- The event log keeps 30 days.

---

Part of the banta API documentation. Full index: https://docs.banta.co.il/llms.txt
