Docs / Sending events

Sending events

Events go to one endpoint, in three spellings. Send them from your server, never from a browser or an app people can unpack: a token can write to your totals.

Routes

The base URL is https://api.bumpbunny.dev/v1. All three routes do the same thing.

Route Use
POST /events The general one.
POST /track Same as /events, for code that reads better that way.
POST /track/{trackable} The trackable comes from the URL, for every event in the body.

Send the token as a bearer token in the Authorization header. Bump never reads a token from the URL or the query string, because URLs end up in logs.

With /track/{trackable}, an empty body counts one:

curl -X POST https://api.bumpbunny.dev/v1/track/signups \
  -H "Authorization: Bearer bump_test_…"
{
  "object": "list",
  "data": [
    {
      "object": "event",
      "id": "evt_iylfk1bvez0wt8bv",
      "status": "accepted",
      "milestones": []
    }
  ]
}

When the URL names a trackable, a trackable in the body is ignored.

Body shapes

The body is JSON in one of three shapes. Bump reads them all the same way.

A single event:

{ "trackable": "orders", "identifiable": "usr_123", "amount": 49.99 }

A list of events:

[
  { "trackable": "orders", "identifiable": "usr_123", "amount": 49.99 },
  { "trackable": "members", "identifiable": "team_8" }
]

An object with an events list. It's the only shape that takes options for the whole request, which today means backfill:

{
  "backfill": false,
  "events": [
    { "trackable": "orders", "identifiable": "usr_123", "amount": 49.99 }
  ]
}

Fields

Only trackable is required. Unknown fields are ignored, and null counts as left out.

Field Rules
trackable Required. What you count, like orders. Lowercase letters, digits, _, . and -, up to 64 characters. Created on first use.
identifiable Who or what the event belongs to. Left out, the event only counts project-wide. See Identifiables.
count A whole number, negative allowed. Defaults to 1. Up to a billion either way. A list expands into one event per value.
amount A number in the project's currency, up to 2 decimals, negative allowed. A string like "49.99" works too. Defaults to 0. Up to a billion either way. A list expands into one event per value.
occurred_at When it happened, ISO 8601 with an offset. Defaults to when Bump receives it. See occurred_at.
id Your idempotency key, up to 64 characters. See Idempotency.
activity true or false. Overrides the trackable's activity setting for this event.
note Text up to 255 characters, kept with the event.

Identifiables

An identifiable is the customer, team or account an event belongs to, named by your own ID. Bump calls that ID the handle and stores it exactly as sent. Five ways to write it, all in one valid request:

[
  { "trackable": "orders", "identifiable": "usr_123" },
  { "trackable": "orders", "identifiable": ["usr_123", "team_8"] },
  { "trackable": "orders", "identifiable": { "team_8": "Acme BV" } },
  { "trackable": "orders", "identifiable": { "handle": "team_8", "label": "Acme BV" } },
  { "trackable": "orders", "identifiable": ["usr_123", { "team_8": "Acme BV" }] }
]
  • A handle is 1 to 64 characters. It can't start or end with a space or hold control characters. A number works too and is stored as text.
  • Case matters: usr_1 and USR_1 are two identifiables.
  • An event takes up to 10 identifiables. It counts once for each of them and once project-wide, never twice.
  • A label is the name the dashboard shows, up to 255 characters. The latest one sent wins. Leaving it out, or sending an empty one, keeps the label Bump has.
  • Labels are for display. Company and team names are fine, person names are your call. An email address never belongs in a handle.

Forgetting an identifiable

When one of your users asks to be forgotten, Bump can forget their identifiable. The handle becomes a random placeholder, the label is removed, and the webhook payloads Bump kept get the placeholder too. Totals and milestones stay, shown as Forgotten in the dashboard. A later event with the same handle starts a new identifiable at zero. An event that names the placeholder is rejected with parameter_invalid and "This handle belongs to a forgotten identifiable."

It takes a token that can forget: switch on Can forget identifiables when you make it in Developers > API tokens. It's off by default, and an existing token can't get it later, so make a new one. Owners and admins can also forget from the identifiable's page in the dashboard.

The handle goes in the body, never in the URL:

curl https://api.bumpbunny.dev/v1/identifiables/forget \
  -H "Authorization: Bearer bump_test_…" \
  -H "Content-Type: application/json" \
  -d '{"identifiable": "usr_123"}'
{
  "object": "identifiable",
  "handle": "forgotten_7pvls8789jo4fjrq",
  "forgotten": true
}

A handle the token's environment doesn't have, including one already forgotten, gets 404:

{
  "error": {
    "type": "invalid_request_error",
    "code": "resource_missing",
    "message": "No identifiable with that handle in this environment.",
    "param": "identifiable"
  }
}

Forgetting happens in the token's environment only. To forget someone in live and in test, call it with a token of each.

With the Laravel package, it's Bump::forget($user).

Count and amount

Every trackable keeps two totals: a count and an amount. An order is one order and 49.99 at the same time, so the defaults do most of the work: count is 1 unless you say otherwise, amount is 0.

Bump never edits an event once it's in. To correct a total, send the difference as a new event. A partial refund on an order that still counts:

{ "trackable": "orders", "identifiable": "usr_123", "count": 0, "amount": -25.00, "note": "partial refund", "activity": false }

Send "count": 0 here, or the refund adds an order.

Amounts are stored as whole cents, so 49.99 stays 49.99. A JavaScript float like 59.97000000000001 reads as 59.97. A third decimal that isn't zero is rejected.

Lists

A list in count or amount expands into one event per value. This is two orders, one of 10.00 and one of 25.50:

curl https://api.bumpbunny.dev/v1/events \
  -H "Authorization: Bearer bump_test_…" \
  -H "Content-Type: application/json" \
  -d '{"id": "cart-77", "trackable": "orders", "identifiable": "usr_123", "amount": [10, 25.50]}'
{
  "object": "list",
  "data": [
    {
      "object": "event",
      "id": "evt_fws0obu8adzch1ok",
      "status": "accepted",
      "milestones": []
    },
    {
      "object": "event",
      "id": "evt_9k494aj0thleef0s",
      "status": "accepted",
      "milestones": []
    }
  ]
}
  • Each value counts toward the 100-event limit.
  • count and amount can't both be lists in one event.
  • With an id, each value gets its own key, cart-77:0 and cart-77:1, so a retry is matched value by value.

Idempotency

Give each event an id from your side, like the order number, and send the same id when you retry. Bump records the first one and answers duplicate to every repeat. A duplicate never changes a total.

curl https://api.bumpbunny.dev/v1/events \
  -H "Authorization: Bearer bump_test_…" \
  -H "Content-Type: application/json" \
  -d '{"id": "order-1042", "trackable": "orders", "identifiable": "usr_123", "amount": 49.99}'

The first time:

{
  "object": "list",
  "data": [
    {
      "object": "event",
      "id": "evt_d1tpmv5xzky1rspf",
      "status": "accepted",
      "milestones": []
    }
  ]
}

The same call again answers with the original event's ID:

{
  "object": "list",
  "data": [
    {
      "object": "event",
      "id": "evt_d1tpmv5xzky1rspf",
      "status": "duplicate",
      "milestones": []
    }
  ]
}
  • Keys are unique per environment. The same id in live and in test is two events, so a backfill tried in test first doesn't block the live one.
  • Without an id, every call counts, retries included.
  • Up to 64 characters. Case matters.

occurred_at

When the event happened. Leave it out and Bump uses the moment it receives the event. Send it when the event happened earlier, like a queued job that ran late, and the event lands on its own day.

  • ISO 8601 with an offset, like 2026-10-02T14:03:11+02:00 or 2026-10-02T12:03:11Z. A time without an offset is rejected, because it could mean any timezone.
  • Fractions of a second are dropped.
  • Up to 5 minutes ahead of Bump's clock, for clocks that run a little fast. Anything later is rejected.
  • Days follow the project's timezone.

activity and note

Active today, Last 7 days and Last 30 days on the overview count the identifiables with at least one event that counts as activity. Each trackable has a setting for that, on by default, on the trackable's page. activity overrides it for one event: send false for things that happened without the person doing anything, like a renewal or a refund, and true to count one anyway.

Events without an identifiable never count as activity.

note is text up to 255 characters, kept with the event and shown in Developers > Events. It's a good place for why a correction happened.

Backfill

To import history, use the object shape with "backfill": true. It applies to every event in the request.

curl https://api.bumpbunny.dev/v1/events \
  -H "Authorization: Bearer bump_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "backfill": true,
    "events": [
      { "id": "order-1001", "trackable": "orders", "identifiable": "usr_123", "amount": 49.99, "occurred_at": "2026-03-14T10:22:00+01:00" },
      { "id": "order-1002", "trackable": "orders", "identifiable": "team_8", "amount": 120.00, "occurred_at": "2026-03-15T16:05:00+01:00" }
    ]
  }'
{
  "object": "list",
  "data": [
    {
      "object": "event",
      "id": "evt_da7xxp4vuj9rt5vx",
      "status": "accepted",
      "milestones": []
    },
    {
      "object": "event",
      "id": "evt_jvmxw9bp1lhe2imj",
      "status": "accepted",
      "milestones": []
    }
  ]
}

Backfilled events count like any other: totals, charts and active figures include them. What's different:

  • Milestones they reach are recorded and marked as backfilled, but never sent to integrations. A first import doesn't send your customers a year of old news.
  • A hold period counts from the event's occurred_at, not from the import.
  • They don't move Tracking since, the time of the earliest event counted live, shown in Developers > Events. Import everything before that moment and nothing after, so history and live counting meet without overlap. Environments has more.
  • backfill on a single event is rejected. It belongs on the request.

There's no undo in the dashboard. To take a backfill back out, for example to run a corrected one, email [email protected]. It shows in Developers > Audit log afterwards.

Limits

  • Up to 100 events per request, counting each value in a count or amount list. A bigger request is refused as a whole.
  • 600 requests a minute per token. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. Over the limit, Bump answers 429 with a Retry-After header in seconds.
  • Bump processes the events while you wait, one by one. If something fails halfway, the events before it stay recorded, and a retry with the same ids only adds the rest.

Response

Once Bump can read the request, it answers 200 with one result per event, in order, lists expanded. One request with all three outcomes, sent after the idempotency example above:

[
  { "id": "order-1043", "trackable": "orders", "identifiable": "usr_123", "amount": 19.99 },
  { "id": "order-1042", "trackable": "orders", "identifiable": "usr_123", "amount": 49.99 },
  { "id": "order-1044", "trackable": "orders", "identifiable": "usr_123", "amount": 9.999 }
]

Bump answers:

{
  "object": "list",
  "data": [
    {
      "object": "event",
      "id": "evt_6a8fugl6wqem7afr",
      "status": "accepted",
      "milestones": []
    },
    {
      "object": "event",
      "id": "evt_d1tpmv5xzky1rspf",
      "status": "duplicate",
      "milestones": []
    },
    {
      "object": "event",
      "status": "rejected",
      "error": {
        "type": "invalid_request_error",
        "code": "parameter_invalid",
        "message": "Amounts take at most 2 decimals.",
        "param": "events[2].amount"
      }
    }
  ]
}
Status Means
accepted Recorded. id is the event's ID, milestones the IDs of milestones it reached.
duplicate Its id was sent before. Nothing changed. id is the original event's ID.
rejected Not recorded. error says why. The other events in the request carry on.

Totals aren't part of the response. Reading totals shows how to get them. Errors has every code.