# Events

> Send what your users do in your product (sign-ups, trials, payments, logins) to start, stop and personalize automations. Idempotent, on every plan.

Source: https://docs.pidgely.com/guides/events/

Events are the things your users do in your product: `user_signed_up`, `trial_started`, `payment_failed`, `login`. Send them to Pidgely with one request and they:

- create or update the **contact** (name and properties), so your contacts stay current without a separate sync;
- start [automations](https://docs.pidgely.com/guides/automations/) with the **custom event** trigger, end them when a **goal** or **exit event** arrives, and resume **wait for event** steps;
- feed the **inactivity** trigger (no `login` for 14 days);
- are listed in the dashboard (**Marketing → Events**) per contact, live.

Events work on **every plan**. Automations need Scale, but the events you record before upgrading are kept, so contacts and activity are ready when you do.

## Send an event

[`POST /events`](https://docs.pidgely.com/api-reference/events/record/) with a `full_access` or `sending_access` [API key](https://docs.pidgely.com/authentication/):

```bash tab="cURL"
curl -X POST https://api.pidgely.com/events \
  -H "Authorization: Bearer $PIDGELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "trial_started",
    "email": "ana.souza@example.com",
    "contact": { "first_name": "Ana", "last_name": "Souza", "properties": { "plan": "pro", "trial_ends_at": "2026-11-05" } },
    "properties": { "plan": "pro", "trial_days": 14 },
    "idempotency_key": "trial-started-8812"
  }'
```

```js tab="Node.js"
const res = await fetch('https://api.pidgely.com/events', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.PIDGELY_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'trial_started',
    email: 'ana.souza@example.com',
    contact: { first_name: 'Ana', last_name: 'Souza', properties: { plan: 'pro', trial_ends_at: '2026-11-05' } },
    properties: { plan: 'pro', trial_days: 14 },
    idempotency_key: 'trial-started-8812',
  }),
});
if (!res.ok) throw new Error(`pidgely: ${res.status} ${await res.text()}`);
const event = await res.json(); // { object: 'event', id, contact_id, duplicate, ... }
```

```python tab="Python"
import os
import requests

res = requests.post(
    "https://api.pidgely.com/events",
    headers={"Authorization": f"Bearer {os.environ['PIDGELY_API_KEY']}"},
    json={
        "name": "trial_started",
        "email": "ana.souza@example.com",
        "contact": {"first_name": "Ana", "last_name": "Souza", "properties": {"plan": "pro", "trial_ends_at": "2026-11-05"}},
        "properties": {"plan": "pro", "trial_days": 14},
        "idempotency_key": "trial-started-8812",
    },
    timeout=10,
)
res.raise_for_status()
event = res.json()
```

```go tab="Go"
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	body, _ := json.Marshal(map[string]any{
		"name":  "trial_started",
		"email": "ana.souza@example.com",
		"contact": map[string]any{
			"first_name": "Ana",
			"last_name":  "Souza",
			"properties": map[string]any{"plan": "pro", "trial_ends_at": "2026-11-05"},
		},
		"properties":      map[string]any{"plan": "pro", "trial_days": 14},
		"idempotency_key": "trial-started-8812",
	})
	req, _ := http.NewRequest("POST", "https://api.pidgely.com/events", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("PIDGELY_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	var event map[string]any
	json.NewDecoder(res.Body).Decode(&event)
	fmt.Println(res.Status, event["id"])
}
```

The answer is the recorded event, with the contact it belongs to:

```json
{
  "object": "event",
  "id": "0199e4a2-7b8c-7d9e-8f0a-1b2c3d4e5f61",
  "name": "trial_started",
  "email": "ana.souza@example.com",
  "contact_id": "0199d7f4-5d7b-7a2c-b4e1-9f3c2a6d8e05",
  "properties": { "plan": "pro", "trial_days": 14 },
  "occurred_at": "2026-10-22T12:00:00Z",
  "received_at": "2026-10-22T12:00:00.184Z",
  "duplicate": false
}
```

Send events from your backend, where the API key is safe; never from the browser or a mobile app.

## Fields

| Field | Required | |
|---|---|---|
| `name` | yes | 1 to 100 characters: letters, digits, `_`, `.`, `:` and `-`. Use a stable past-tense name per action (`subscription_started`, `invoice.paid`). Names are case-sensitive. |
| `email` | yes | The contact's address. The contact is created when it does not exist yet (source `event`). |
| `contact` | no | `first_name`, `last_name` and `properties` to set on the contact. Properties merge with the existing ones (`null` removes one) and follow the [contact property rules](https://docs.pidgely.com/guides/contacts/#contacts). Dates (`trial_ends_at`, `renewal_at`, `birthday`) as `YYYY-MM-DD` or RFC 3339 drive the **contact date** trigger. Events never change consent: an unsubscribed contact stays unsubscribed. |
| `properties` | no | Data of this event, a flat JSON object of strings, numbers, booleans or `null`, up to 50 keys and 16 KB. Available in automation e-mails as `{{{event.<key>}}}`; a goal reads its revenue from one of them (`amount`, in major units: `49.90`). |
| `occurred_at` | no | When it happened, RFC 3339 (default: now). At most 5 minutes in the future. |
| `idempotency_key` | no | Up to 256 characters. The same key within 30 days returns the first event with `duplicate: true` and records nothing new. The `Idempotency-Key` header works too. |

### Historic events

An event whose `occurred_at` is more than **24 hours** old is recorded, updates the contact and its last activity (for the inactivity trigger), and can end flows through goals, but it **starts no automation**. Importing a year of history never mails anyone.

## Send many events

[`POST /events/batch`](https://docs.pidgely.com/api-reference/events/record-batch/) takes up to 100 events. Each is handled like a single event; invalid ones are reported by index in `errors` and the others are recorded:

```bash
curl -X POST https://api.pidgely.com/events/batch \
  -H "Authorization: Bearer $PIDGELY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "events": [
        { "name": "login", "email": "ana.souza@example.com" },
        { "name": "payment_succeeded", "email": "bruno@example.com", "properties": { "amount": 49.9, "plan": "pro" } }
      ] }'
```

```json
{ "data": [{ "index": 0, "id": "0199e4a2-…", "duplicate": false }, { "index": 1, "id": "0199e4a2-…", "duplicate": false }], "errors": [] }
```

## Rate limits and retries

Event requests count toward your organization's [API rate limit](https://docs.pidgely.com/rate-limits/) like any other request (a batch counts once). On `429`, wait for `Retry-After` and retry with the same `idempotency_key`: a retried event is never recorded twice. For high volumes, buffer events and send batches of up to 100.

## Recommended events for SaaS

These are the names the [SaaS presets](https://docs.pidgely.com/guides/automations/#for-saas-and-apps) expect; you can rename them in each automation.

| Event | When | Useful properties |
|---|---|---|
| `user_signed_up` | An account is created | `plan`, `source` |
| `activated` | The user reached the first value (first project, first send) | |
| `login` | Each sign-in (or once a day) | |
| `checkout_started` | The user opened your checkout or pricing page | `plan`, `checkout_url` |
| `subscription_started` | A paid subscription starts | `plan`, `amount`, `currency` |
| `payment_failed` | A renewal charge failed | `amount`, `payment_url` |
| `payment_succeeded` | A charge (or retry) succeeded | `amount` |

Keep `trial_ends_at` (and `renewal_at`) up to date as contact properties: send them in `contact.properties` with `trial_started`, or update the [contact](https://docs.pidgely.com/guides/contacts/) directly.

## See your events

The dashboard's **Marketing → Events** page lists recent events (newest first, live), filters by contact and by name, shows the names your product has sent, and has the snippet above with your API base URL. Through the API: [`GET /events`](https://docs.pidgely.com/api-reference/events/list/) (`email`, `name`, cursor pagination) and [`GET /events/names`](https://docs.pidgely.com/api-reference/events/list-names/), with a `full_access` key.

Events are kept as long as your e-mail metadata, then deleted month by month.
