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

# HTTP ingest API

> Send events directly to the ingest endpoint from platforms without an SDK.

All SDKs send the same JSON to this endpoint. Use it directly from platforms without a Glyph SDK.

```
POST https://in.glyphhq.io/api/v1/ingest
Content-Type: application/json
```

<Note>
  Prefer an SDK when one is available. The SDKs add client-side validation, batching, retry with backoff, and automatic `messageId` generation. The formal wire contract is included in [JSON Schema](#json-schema).
</Note>

## Authentication

Send the write key as a bearer token or with `X-Glyph-Write-Key`:

```
Authorization: Bearer glyph_pk_...
```

```
X-Glyph-Write-Key: glyph_pk_...
```

If both headers are present they must match; conflicting values are rejected with `400`. A missing, invalid, revoked, or rotated key returns `401`.

The endpoint allows cross-origin requests from any origin (`Access-Control-Allow-Origin: *`), so browsers can post to it directly.

## Request body

The payload is either a single message object, or a batch:

```json theme={"dark"}
{ "batch": [ { ... }, { ... } ] }
```

Batches contain at most **100** messages and run in a single database transaction. The entire batch commits or rolls back.

## Fields shared by every message

| Field       | Type             | Required | Description                                                                                                                                                                                      |
| ----------- | ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type`      | string           | Yes      | `"profile"`, `"track"`, `"group"`, or `"page"`.                                                                                                                                                  |
| `userId`    | string           | Yes      | Your stable customer identifier. 1–256 characters.                                                                                                                                               |
| `messageId` | string           | No       | Deduplication key, 1–256 characters. A message whose `messageId` was already accepted for your workspace is skipped, so retried requests aren't double-counted. See [idempotency](#idempotency). |
| `timestamp` | string \| number | No       | ISO 8601 date-time or epoch **milliseconds**. Defaults to arrival time; set it when backfilling. An unparseable value is rejected with `422`.                                                    |

Unknown fields return `422`; each message type accepts only its documented keys.

<Warning>
  Numeric timestamps are epoch **milliseconds**, not seconds. A value in epoch seconds is interpreted as a date in early 1970.
</Warning>

## `profile`

Upserts traits onto a customer. `userId` is the durable identity key. `traits.email` is required so Glyph can recognize the customer and provide an outreach path. Traits merge shallowly with what's already stored: keys you send overwrite, keys you omit are preserved.

```json theme={"dark"}
{
  "type": "profile",
  "userId": "user_123",
  "messageId": "1f6bcd2e-8f6a-4f1e-9d3a-0c5a1f2b3c4d",
  "traits": {
    "email": "sarah@example.com",
    "plan": "pro"
  }
}
```

## `track`

Records something a customer did. `event` is required (1–256 characters); `properties` is any JSON object and defaults to `{}`. Tracking an unknown `userId` creates the customer record automatically, with no traits until a `profile` arrives.

```json theme={"dark"}
{
  "type": "track",
  "userId": "user_123",
  "event": "Invoice paid",
  "properties": {
    "amount": 4900,
    "currency": "USD"
  }
}
```

## `group`

Records account or team context as an event on the customer's timeline. `groupId` is required (1–256 characters); `traits` defaults to `{}`. Glyph does not currently create a separate account record or provide group-level analytics.

```json theme={"dark"}
{
  "type": "group",
  "userId": "user_123",
  "groupId": "acme_inc",
  "traits": { "plan": "enterprise" }
}
```

## `page`

Records a page view. `name` is optional but must be non-empty when present. Omit the field rather than sending an empty string.

```json theme={"dark"}
{
  "type": "page",
  "userId": "user_123",
  "name": "Dashboard",
  "properties": { "path": "/dashboard" }
}
```

## Example request

```bash theme={"dark"}
curl https://in.glyphhq.io/api/v1/ingest \
  -H "Authorization: Bearer $GLYPH_WRITE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "track",
    "userId": "user_123",
    "event": "Invoice paid",
    "properties": { "amount": 4900 }
  }'
```

## Responses

The server sends a response after the database transaction commits. Retry non-`2xx` responses and timeouts according to [Idempotency](#idempotency).

| Status | Meaning                                                                                                      |
| ------ | ------------------------------------------------------------------------------------------------------------ |
| `202`  | Validated and processed. Body is `{ "accepted": <count> }`, where the count is the number of input messages. |
| `400`  | Malformed JSON, or conflicting `Authorization` and `X-Glyph-Write-Key` headers.                              |
| `401`  | Missing, invalid, revoked, or rotated write key.                                                             |
| `403`  | Test environment event cap reached (see [Write keys](/docs/reference/write-keys#test-environment-limits)).        |
| `422`  | Invalid payload. The body includes an `issues` array describing each validation failure.                     |
| `429`  | Rate limited. Includes a `Retry-After` header in seconds.                                                    |
| `5xx`  | Temporary server failure. Retry with backoff and the same `messageId`.                                       |

<Note>
  A `202` does not mean every input produced new visible data. Repeated `messageId` values are deduplicated. Messages for **new** customers beyond the plan's profile limit are skipped while messages for existing customers continue to work. See [Troubleshooting](/docs/guides/troubleshooting#youve-hit-your-plans-customer-profile-limit).
</Note>

## Request provenance

Glyph records the authenticated write key, its name, and its configured purpose with each accepted request. Create a separate key with the **Direct HTTP** purpose for this integration rather than reusing a browser or server key.

Official Glyph SDKs also report their package name and version. Direct HTTP integrations should not imitate this metadata. A request without SDK metadata is stored with an unknown client; the write key remains the authoritative source.

## Rate limits

Each write key has a default budget of **600 messages per 60-second sliding window**; some plans have a higher limit. The budget counts messages rather than requests, so a batch of 100 consumes 100. Exceeding the budget returns `429` with a `Retry-After` header, which the SDKs honor automatically.

Pace sustained imports below the budget. The [backfill guide](/docs/guides/backfill#work-within-the-rate-limit) includes an example.

## Idempotency

If you implement your own delivery, mirror the SDKs' behavior:

* Retry `408`, `429`, `500`, `502`, `503`, and `504` (and network failures) with exponential backoff, honoring `Retry-After` when present.
* Treat other errors as permanent: `401` means a bad key, `422` an invalid payload.
* Send a `messageId` and reuse it when retrying, so deliveries stay idempotent.

`messageId` deduplication is also what makes re-runnable imports possible: derive stable IDs from your source data (for example `import-invoice-1042`) and running the same import twice stores each message once. The SDKs generate a fresh UUID per call, so this technique is only available over HTTP.

## JSON Schema

You usually won't need the schema when using an SDK. Use it to validate direct HTTP integrations, generate types, or build tooling around the ingest endpoint.

<Accordion title="View the ingest v1 JSON Schema">
  ```json theme={"dark"}
  {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://glyphhq.io/contracts/ingest-v1.schema.json",
    "title": "Glyph ingest v1",
    "oneOf": [
      { "$ref": "#/$defs/message" },
      {
        "type": "object",
        "additionalProperties": false,
        "required": ["batch"],
        "properties": {
          "batch": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": { "$ref": "#/$defs/message" }
          }
        }
      }
    ],
    "$defs": {
      "jsonObject": {
        "type": "object",
        "additionalProperties": true
      },
      "profileTraits": {
        "type": "object",
        "additionalProperties": true,
        "required": ["email"],
        "properties": {
          "email": { "type": "string", "minLength": 1, "maxLength": 256 }
        }
      },
      "timestamp": {
        "oneOf": [
          { "type": "string", "format": "date-time" },
          { "type": "number" }
        ]
      },
      "baseMessage": {
        "type": "object",
        "required": ["type", "userId"],
        "properties": {
          "type": { "type": "string" },
          "userId": { "type": "string", "minLength": 1, "maxLength": 256 },
          "messageId": { "type": "string", "minLength": 1, "maxLength": 256 },
          "timestamp": { "$ref": "#/$defs/timestamp" },
          "context": { "$ref": "#/$defs/jsonObject" }
        }
      },
      "profile": {
        "allOf": [
          { "$ref": "#/$defs/baseMessage" },
          {
            "type": "object",
            "additionalProperties": false,
            "required": ["type", "traits"],
            "properties": {
              "type": { "const": "profile" },
              "userId": { "type": "string", "minLength": 1, "maxLength": 256 },
              "messageId": { "type": "string", "minLength": 1, "maxLength": 256 },
              "timestamp": { "$ref": "#/$defs/timestamp" },
              "context": { "$ref": "#/$defs/jsonObject" },
              "traits": { "$ref": "#/$defs/profileTraits" }
            }
          }
        ]
      },
      "track": {
        "allOf": [
          { "$ref": "#/$defs/baseMessage" },
          {
            "type": "object",
            "additionalProperties": false,
            "required": ["type", "event"],
            "properties": {
              "type": { "const": "track" },
              "userId": { "type": "string", "minLength": 1, "maxLength": 256 },
              "messageId": { "type": "string", "minLength": 1, "maxLength": 256 },
              "timestamp": { "$ref": "#/$defs/timestamp" },
              "context": { "$ref": "#/$defs/jsonObject" },
              "event": { "type": "string", "minLength": 1, "maxLength": 256 },
              "properties": { "$ref": "#/$defs/jsonObject" }
            }
          }
        ]
      },
      "group": {
        "allOf": [
          { "$ref": "#/$defs/baseMessage" },
          {
            "type": "object",
            "additionalProperties": false,
            "required": ["type", "groupId"],
            "properties": {
              "type": { "const": "group" },
              "userId": { "type": "string", "minLength": 1, "maxLength": 256 },
              "messageId": { "type": "string", "minLength": 1, "maxLength": 256 },
              "timestamp": { "$ref": "#/$defs/timestamp" },
              "context": { "$ref": "#/$defs/jsonObject" },
              "groupId": { "type": "string", "minLength": 1, "maxLength": 256 },
              "traits": { "$ref": "#/$defs/jsonObject" }
            }
          }
        ]
      },
      "page": {
        "allOf": [
          { "$ref": "#/$defs/baseMessage" },
          {
            "type": "object",
            "additionalProperties": false,
            "required": ["type"],
            "properties": {
              "type": { "const": "page" },
              "userId": { "type": "string", "minLength": 1, "maxLength": 256 },
              "messageId": { "type": "string", "minLength": 1, "maxLength": 256 },
              "timestamp": { "$ref": "#/$defs/timestamp" },
              "context": { "$ref": "#/$defs/jsonObject" },
              "name": { "type": "string", "minLength": 1, "maxLength": 256 },
              "properties": { "$ref": "#/$defs/jsonObject" }
            }
          }
        ]
      },
      "message": {
        "oneOf": [
          { "$ref": "#/$defs/profile" },
          { "$ref": "#/$defs/track" },
          { "$ref": "#/$defs/group" },
          { "$ref": "#/$defs/page" }
        ]
      }
    }
  }
  ```
</Accordion>
