> ## Documentation Index
> Fetch the complete documentation index at: https://docs.livingsecurity.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Push Source API

> Register a source and send JSON or CSV event batches to Living Security with a scoped API access key.

Use a Custom Push Source when a system sends records to Living Security instead of being polled by a Catalog connector. The sender can submit JSON arrays or CSV files to a regional HTTPS endpoint.

## Before you begin

You need:

* permission to manage integrations and access keys in Living Security
* a server-side sender that can make HTTPS `POST` requests
* the Living Security region for your organization
* a stable source name and at least one stream name

Source names and stream names are permanent routing keys. Source names must be 1–64 lowercase letters, digits, or hyphens. Stream names also allow underscores. Both must start with a letter or digit.

<Warning>
  Treat the API access key as a secret. Do not place it in browser code, commit it
  to source control, or send it in a support ticket.
</Warning>

## 1. Register the source and streams

<Steps>
  <Step title="Open Custom Push Source">
    Go to **Settings → Integrations → Catalog**, find **Custom Push Source**, and select **Add**.
  </Step>

  <Step title="Name the source">
    Enter the source routing key and an optional description. For example, use `custom-phishing-sim` rather than a display name with spaces.
  </Step>

  <Step title="Add streams">
    Add each stream the sender will use. The source and stream pair must exist before the first push; an unknown pair returns `404 Not Found`.
  </Step>

  <Step title="Save the source">
    After creation, open the source under **Settings → Integrations → Connected**. Its **Push via API** view shows the endpoint pattern and recent runs.
  </Step>
</Steps>

## 2. Create a push-ingest key

Go to **Settings → Access Keys**, select **Create Access Key**, and choose the **Push Ingest** scope. Copy the generated value immediately; it is shown only once.

The key is bound to one Living Security organization. The push request does not need an organization header. See [API Access Keys](/features/settings/api-keys) for rotation and revocation.

## 3. Choose the regional endpoint

Use the hostname for the Living Security organization:

| Region | Base URL |
| - | - |
| United States | `https://api.us-east-1.livingsecurity.ai/api/v1` |
| European Union | `https://api.eu-west-1.livingsecurity.ai/api/v1` |

Append `/push-ingest/{integrationKey}/{streamName}`. The path is case-sensitive and must exactly match the registered source and stream.

<ParamField path="integrationKey" type="string" required>
  The registered source name. Use 1–64 lowercase letters, digits, or hyphens, starting with a letter or digit.
</ParamField>

<ParamField path="streamName" type="string" required>
  The registered stream name. The same rules apply, and underscores are also allowed.
</ParamField>

<ParamField header="Authorization" type="string" required>
  Send the push-ingest access key as `Bearer <access-key>`.
</ParamField>

<ParamField header="Content-Type" type="string">
  Use `application/json` for JSON or `text/csv` for CSV. The server can also detect a JSON array when this header is omitted.
</ParamField>

<ParamField header="x-organization-id" type="string">
  Optional organization UUID. The access key already determines the organization. If you send this header, it must match the key's organization; it cannot select another organization.
</ParamField>

### Request body

Send a top-level, non-empty JSON array of objects or CSV file bytes with a header row. There is no named JSON wrapper field. Each batch can contain up to 10,000 records and 10 MB.

## 4. Send a JSON batch

Send a non-empty JSON array whose elements are objects. `Content-Type: application/json` is recommended and is required by many sending products even though Living Security can detect an array body without it.

<RequestExample>
  ```bash JSON batch theme={null}
  curl --request POST \
    'https://api.us-east-1.livingsecurity.ai/api/v1/push-ingest/custom-phishing-sim/events' \
    --header 'Authorization: Bearer <access-key>' \
    --header 'Content-Type: application/json' \
    --data '[
      {
        "email": "person@example.com",
        "event_at": "2026-09-21T14:30:00Z",
        "action": "reported"
      }
    ]'
  ```

  ```bash CSV file theme={null}
  curl --request POST \
    'https://api.us-east-1.livingsecurity.ai/api/v1/push-ingest/custom-phishing-sim/events' \
    --header 'Authorization: Bearer <access-key>' \
    --header 'Content-Type: text/csv' \
    --data-binary '@events.csv'
  ```
</RequestExample>

A successful request returns `202 Accepted`. The error tab shows a `400 Bad Request` when the JSON body is malformed:

<ResponseExample>
  ```json 202 Accepted theme={null}
  {
    "runId": "01a0c45f-ae40-7e5f-8a9b-0c1d2e3f4a5b",
    "recordsAccepted": 1,
    "integrationKey": "custom-phishing-sim",
    "streamName": "events"
  }
  ```

  ```json 400 Invalid JSON theme={null}
  {
    "statusCode": 400,
    "timestamp": "2026-09-22T14:30:00.000Z",
    "path": "/api/v1/push-ingest/custom-phishing-sim/events",
    "message": "Request body is not valid JSON. Send a JSON array of objects, or set Content-Type: text/csv to push a CSV.",
    "error": "Bad Request"
  }
  ```
</ResponseExample>

Acceptance means the batch was stored for asynchronous processing. It does not mean every record has already been interpreted into activity.
For the `400` example, correct the JSON or send CSV with `Content-Type: text/csv`, then retry.

<ResponseField name="runId" type="string" required>
  The run ID for this accepted batch. Keep it to check processing status.
</ResponseField>

<ResponseField name="recordsAccepted" type="number" required>
  The number of records accepted and stored.
</ResponseField>

<ResponseField name="integrationKey" type="string" required>
  The source name used for this push.
</ResponseField>

<ResponseField name="streamName" type="string" required>
  The stream name used for this push.
</ResponseField>

## Send CSV instead

Set `Content-Type: text/csv` and send the file bytes, as shown in the **CSV file** request example above. The first row supplies field names.

Boolean text becomes a boolean and ordinary numeric text becomes a number. Leading-zero values, unsafe large integers, dates, and other text remain strings. Empty cells omit that field. Malformed rows, unbalanced quotes, and UTF-16 files are rejected.

## Request limits and retries

| Contract | Current value |
| - | - |
| Maximum records | 10,000 per request |
| Maximum request body | 10 MB |
| Per-organization request rate | 600 requests per 60 seconds |

Split larger exports into batches that satisfy both limits. Wait for each response and retain the returned `runId` in sender logs.

| Response | Meaning | Sender action |
| - | - | - |
| `202` | Batch accepted | Record the `runId`; do not immediately resend |
| `400` | Invalid name, JSON, CSV, record count, or body | Correct the request; do not retry unchanged |
| `401` | Missing, expired, revoked, or invalid key | Replace the key and use the Bearer header |
| `403` | Key lacks the required scope | Create a key with **Push Ingest** scope |
| `404` | Source or stream is not registered | Register the exact pair or correct the URL |
| `413` | Request body exceeds 10 MB | Split the batch into smaller requests |
| `429` | Temporary IP-level throttling | Honor `Retry-After` when present and back off |
| `503` | Organization rate bucket or limiter unavailable | Wait for `Retry-After`, then resend the same batch |

The normal authenticated organization limit returns `503`, not `429`, so webhook senders treat it as retriable. A `503` is returned before the batch is stored; resending that same batch after the stated interval does not duplicate an accepted run.

## Verify the data flow

Open the Custom Push Source and review **Push History**. Confirm the stream, record count, key attribution, and processing status. Data with a maintained interpreter can then become people and normalized activity. Data without an interpreter remains stored but does not produce activity.

<Info>
  Use exact field names and payload shape from the provider-specific guide when
  one exists. A successful `202` proves delivery, not that an arbitrary schema
  maps to Living Security activity.
</Info>

## Troubleshooting checklist

* Copy the endpoint from the correct source and replace the stream placeholder.
* Send the access key as `Authorization: Bearer …`; `x-api-key` is not accepted.
* Confirm the body is a top-level JSON array, not a single object or an envelope around the records.
* Confirm the source and stream spelling, including hyphens, underscores, and case.
* Review **Push History** before resending a batch whose response was lost.
* When contacting [Living Security Support](mailto:support@livingsecurity.com), include the organization, source, stream, request time, `runId` when available, and response status. Do not include the access key or event payload.

## Related

<CardGroup cols={2}>
  <Card title="Zscaler ZIA Cloud NSS" icon="shield" href="/integrations/zscaler-zia-cloud-nss">
    Configure a supported Zscaler web-log feed with the exact source, stream, headers, and JSON shape.
  </Card>

  <Card title="API Access Keys" icon="key" href="/features/settings/api-keys">
    Create, rotate, and revoke scoped credentials.
  </Card>

  <Card title="Signal Data Sources" icon="wave-pulse" href="/signals/data-sources">
    Learn how interpreted integration activity supports signals.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.