# Custom events

The things that happen in your product — a trial started, a plan upgraded. You declare the name once, fire it against a contact, and an automation triggering on that name enrols them.

## Definitions and sends

A *definition* is a name your team owns, created with `POST /events` and addressed afterwards by ID or by that name. A *send* is one occurrence: `POST /events/send` records it against exactly one contact and enrols them into every enabled automation whose trigger names it.

The two are separately permissioned on purpose. Declaring, listing, changing and deleting a definition needs a full-access key. **Sending** accepts a key restricted to sending as well, because firing an event is how an integration starts a workflow and is no more powerful than `POST /emails` — so a key you gave your billing service can report that a trial started without also being able to invent event names or read the ones you have.

## Names

Lower-case letters, digits, `.`, `_` and `-`, starting with a letter, at most 100 characters, and unique among your live definitions. A name can never be shaped like a UUID, which is what lets `{identifier}` take either without ambiguity.

The `rasket:` prefix is reserved for the platform's own triggers — `rasket:contact.created`, `rasket:topic.subscribed`, the `rasket:email.*` family and `rasket:segment.entered`. You can trigger an automation on those; you cannot declare one. A name carrying the prefix is `422 validation_error`.

Deleting a definition is soft and frees the name. An automation version still naming it simply never fires again.

## Payloads

A send may carry a `payload` of key/value pairs, at most 16 KB of JSON. When the definition declares a `schema`, every declared key present in the payload has to be its type; a declared key that is absent or `null` passes, and undeclared keys pass untouched.

| Type | Accepts |
| --- | --- |
| `string` | Any string. |
| `number` | A finite JSON number. |
| `boolean` | `true` or `false`. |
| `date` | An ISO 8601 calendar date (`2026-09-11`) or a zoned instant (`2026-09-11T12:00:00Z`) naming a day that exists. |

A value that disagrees is `422 validation_error`, with one entry in `errors[]` per key so you can see all of them at once. A `PATCH` replaces the schema rather than merging into it.

## What a send guarantees

- `202`, not `200`: the event is durable the moment the call returns, and the runs it enrols start on the next tick.
- There is no `Idempotency-Key` here. Two calls are two events and two runs — retry only a call that never answered.
- One send is exactly one run per matching automation, however many times it is redelivered internally.
- An unknown event name, or a contact this team does not have, is `404 not_found`. Exactly one of `contact_id` and `email` must be given; neither or both is `422`.

## Endpoints

### `POST /events`

Declare a name your integrations can fire and your workflows can trigger on.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` (required) | string | Lower-case letters, digits, `.`, `_` and `-`, starting with a letter, at most 100 characters. Unique among your live events. |
| `schema` | object \| null | A flat map from payload key to type: `string`, `number`, `boolean` or `date`. At most 50 keys. A payload sent under this event is checked against it. |

Create an event:

```sh
curl -X POST "https://api.rasket.com/events" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "trial.started",
  "schema": {
    "plan": "string",
    "seats": "number"
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/events", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "trial.started",
    schema: {
      plan: "string",
      seats: "number"
    }
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/events",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "name": "trial.started",
    "schema": {
      "plan": "string",
      "seats": "number"
    }
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "event",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5f01"
}
```

- Names under the reserved `rasket:` prefix are the platform's own triggers — `rasket:contact.created` and its siblings — and are `422 validation_error` here.
- A name already held by one of your live events is `422 validation_error`.
- Declaring a `schema` is optional. Without one, any payload is accepted.

### `GET /events`

Every event you have declared, newest first.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |
| `search` | string | A case-insensitive substring of the event's name, at most 200 characters. `%` and `_` match literally; a blank term is `422 invalid_parameter`. |

List events:

```sh
curl -X GET "https://api.rasket.com/events?limit=20" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/events?limit=20", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/events?limit=20",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5f01",
      "name": "trial.started",
      "schema": {
        "plan": "string",
        "seats": "number"
      },
      "created_at": "2026-09-11T12:00:00.000Z",
      "updated_at": "2026-09-11T12:00:00.000Z"
    }
  ]
}
```

- Requires a full-access key. A key restricted to sending may fire events, not list them.

### `POST /events/send`

Fire one event for one contact. This is what starts a workflow.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `event` (required) | string | The name of a live event of yours. |
| `contact_id` | string | The contact this happened to. Exactly one of `contact_id` or `email`. |
| `email` | string | The contact's address, instead of an ID. Exactly one of the two. |
| `payload` | object | Key/value pairs to carry with the event, at most 16 KB of JSON. Checked against the event's `schema` when it declares one. |

Send an event:

```sh
curl -X POST "https://api.rasket.com/events/send" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "event": "trial.started",
  "contact_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5f02",
  "payload": {
    "plan": "pro",
    "seats": 12
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/events/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    event: "trial.started",
    contact_id: "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5f02",
    payload: {
      plan: "pro",
      seats: 12
    }
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/events/send",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "event": "trial.started",
    "contact_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5f02",
    "payload": {
      "plan": "pro",
      "seats": 12
    }
  },
)

print(response.json())
```

#### Response `202`

```json
{
  "object": "event",
  "event": "trial.started"
}
```

- **202, not 200.** The event is durable the moment this returns; the runs it enrols start on the next tick.
- This route takes no `Idempotency-Key`. Two calls are two events and two runs — retry it only when the first call never answered.
- A key restricted to sending may call this, which is the point: firing an event is no more powerful than sending an email.
- An unknown event name, or a contact this team does not have, is `404 not_found`.

### `GET /events/{identifier}`

By ID or by name.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `identifier` (required) | string | The event's ID, or its name. A name can never be shaped like an ID. |

Retrieve an event:

```sh
curl -X GET "https://api.rasket.com/events/{identifier}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/events/{identifier}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/events/{identifier}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "event",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5f01",
  "name": "trial.started",
  "schema": {
    "plan": "string",
    "seats": "number"
  },
  "created_at": "2026-09-11T12:00:00.000Z",
  "updated_at": "2026-09-11T12:00:00.000Z"
}
```

- `schema` is `null`, not absent, when the event declares none.
- An event that belongs to another team, or that has been deleted, is `404 not_found`.

### `PATCH /events/{identifier}`

Replace the payload schema, or clear it.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `identifier` (required) | string | The event's ID, or its name. A name can never be shaped like an ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `schema` (required) | object \| null | A flat map from payload key to type: `string`, `number`, `boolean` or `date`. At most 50 keys. A payload sent under this event is checked against it. |

Update an event:

```sh
curl -X PATCH "https://api.rasket.com/events/{identifier}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "schema": {
    "plan": "string",
    "seats": "number",
    "trial_ends_at": "date"
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/events/{identifier}", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    schema: {
      plan: "string",
      seats: "number",
      trial_ends_at: "date"
    }
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/events/{identifier}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "schema": {
      "plan": "string",
      "seats": "number",
      "trial_ends_at": "date"
    }
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "event",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5f01"
}
```

- `schema` is required, because it is the only field a `PATCH` can change. Send `null` to clear it.
- The schema is replaced, not merged. A key you leave out is no longer declared.
- An event's name is immutable, so a published workflow never loses the trigger it names.

### `DELETE /events/{identifier}`

Soft-deletes and frees the name.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `identifier` (required) | string | The event's ID, or its name. A name can never be shaped like an ID. |

Delete an event:

```sh
curl -X DELETE "https://api.rasket.com/events/{identifier}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/events/{identifier}", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/events/{identifier}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "event",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5f01",
  "deleted": true
}
```

- A published workflow version still naming it simply never fires again. Runs already in flight are untouched.
- The name is free to declare again afterwards.
