From 33a328f5fcf02cf186815bd25feaa2fd4aca804a Mon Sep 17 00:00:00 2001
From: Charlie Brinicombe <55358291+cbrinicombe13@users.noreply.github.com>
Date: Sat, 6 Sep 2025 00:19:57 +0100
Subject: [PATCH 1/2] Add docs for idempotency
---
api-reference/idempotency.mdx | 65 ++++++++++++++
docs.json | 1 +
platform/events.mdx | 4 +
snippets/idempotent-event-tracking.mdx | 113 +++++++++++++++++++++++++
4 files changed, 183 insertions(+)
create mode 100644 api-reference/idempotency.mdx
create mode 100644 snippets/idempotent-event-tracking.mdx
diff --git a/api-reference/idempotency.mdx b/api-reference/idempotency.mdx
new file mode 100644
index 0000000..bcac58f
--- /dev/null
+++ b/api-reference/idempotency.mdx
@@ -0,0 +1,65 @@
+---
+title: Idempotency
+description: Prevent unintended side-effects when retrying requests with idempotency controls built into the Trophy API and SDKs.
+"og:description": Prevent unintended side-effects when retrying requests with idempotency built into the Trophy API and SDKs using the `Idempotency-Key` request header.
+icon: repeat
+---
+
+import IdempotentEventTracking from "/snippets/idempotent-event-tracking.mdx";
+
+## What is Idempotency?
+
+When describing APIs, [idempotence](https://en.wikipedia.org/wiki/Idempotence) is a property of a particular operation whereby subsequent invocations after the first have no additional effect on the state of the system.
+
+Trophy's [event tracking API](/api-reference/endpoints/metrics/send-a-metric-change-event) can be used to enforce idempotency preventing client retries from having unintended side effects such as overcounting user interactions, or awarding points to users multiple times for the same action.
+
+This is particularly important where rewards are tied to user actions to prevent users from 'gaming the system'.
+
+## Sending Idempotent Requests
+
+To ensure idempotency is respected when sending events to Trophy, include an `Idempotency-Key` header when using the [event tracking API](/api-reference/endpoints/metrics/send-a-metric-change-event). Additionally, all [client SDKs](/api-reference/client-libraries) support idempotency with built-in type safety.
+
+You can choose what to use as your idempotency key, but it should reflect the level of 'uniqueness' that you want Trophy to respect.
+
+
+ For example, if you use Trophy to reward users for completing workouts, and
+ want to make sure each user can only redeem rewards once for each workout, use
+ the unique ID of the workout as your idempotency key.
+
+
+Here's an example of what using an idempotency key looks like:
+
+
+
+## How Idempotency Works
+
+When Trophy detects an idempotency key has been sent with an event, it will first check if the user the event relates to has used it before. It will then proceed to take one of the following actions:
+
+- If Trophy does recognise the user has used the idempotency key before, it will not process the event, returning a `202 Accepted` response. The response will reflect the state of the system when the original event that first used the idempotency key was sent.
+- If instead Trophy detects the user hasn't used the idempotency key before, it will process the event as usual, returning a `201 Created` response. Finally Trophy will store the idempotency key for lookup during any subsequent requests.
+
+
+ All Trophy [metrics](/platform/metrics) manage idempotency in isolation.
+ Trophy will accept a user using the same idempotency key for events against
+ different metrics as seperate isolated requests.
+
+
+Additionally, when using an idempotency key the response will contain two properties to help clients manage replayed requests effectively:
+
+```json
+{
+ ...,
+ "idempotentReplayed": true, // true if replayed, false otherwise
+ "idempotencyKey": "test" // the original idempotency key if recognised, null otherwise
+}
+```
+
+
+ By default Trophy uses an infinte time window for detecting duplicate events.
+ If you feel you need different behavoir, please [get in
+ touch](mailto:support@trophy.so) and we'll happily set that up for you.
+
+
+## Get Support
+
+Want to get in touch with the Trophy team? Reach out to us via [email](mailto:support@trophy.so). We're here to help!
diff --git a/docs.json b/docs.json
index 4e499f5..1c57e67 100644
--- a/docs.json
+++ b/docs.json
@@ -113,6 +113,7 @@
"api-reference/introduction",
"api-reference/authentication",
"api-reference/rate-limiting",
+ "api-reference/idempotency",
"api-reference/client-libraries"
]
},
diff --git a/platform/events.mdx b/platform/events.mdx
index 3749dfb..36e65c2 100644
--- a/platform/events.mdx
+++ b/platform/events.mdx
@@ -194,6 +194,10 @@ Watch Charlie integrate metric tracking into a simple NextJS application using t
>
+### Idempotent Events
+
+Trophy supports preventing unintended side effects from duplicate events through an `Idempotency-Key` header. [Learn more about idempotency](/api-reference/idempotency).
+
## Get Support
Want to get in touch with the Trophy team? Reach out to us via [email](mailto:support@trophy.so). We're here to help!
diff --git a/snippets/idempotent-event-tracking.mdx b/snippets/idempotent-event-tracking.mdx
new file mode 100644
index 0000000..062721e
--- /dev/null
+++ b/snippets/idempotent-event-tracking.mdx
@@ -0,0 +1,113 @@
+
+```bash cURL {3}
+curl -X POST https://app.trophy.so/api/metrics/words-written/event \
+ -H "X-API-KEY: " \
+ -H "Idempotency-Key: " \
+ -H "Content-Type: application/json" \
+ -d '{
+ "user": {
+ "id": "18",
+ "email": "user@example.com",
+ "tz": "Europe/London"
+ },
+ "value": 750
+}'
+```
+
+```typescript Node {11-13}
+trophy.metrics.event(
+ "words-written",
+ {
+ user: {
+ id: "18",
+ email: "jk.rowling@harrypotter.com",
+ tz: "Europe/London",
+ },
+ value: 750,
+ },
+ {
+ idempotencyKey: "myIdempotencyKey",
+ }
+);
+```
+
+```python Python
+client.metrics.event(
+ key="words-written",
+ user=EventRequestUser(
+ id="18",
+ email="jk.rowling@harrypotter.com",
+ tz="Europe/London",
+ ),
+ value=750.0,
+)
+```
+
+```php PHP
+$user = new EventRequestUser([
+ 'id' => '18',
+ 'email' => 'jk.rowling@harrypotter.com'
+]);
+
+$request = new MetricsEventRequest([
+ 'user' => $user,
+ 'value' => 750
+]);
+
+$trophy->metrics->event("words-written", $request);
+```
+
+```java Java
+MetricsEventRequest request = MetricsEventRequest.builder()
+ .user(
+ EventRequestUser.builder()
+ .id("18")
+ .email("jk.rowling@harrypotter.com")
+ .build()
+ )
+ .value(750)
+ .build();
+
+EventResponse response = client.metrics().event("words-written", request);
+```
+
+```go Go
+response, err := client.Metrics.Event(
+ context.TODO(),
+ "words-written",
+ &api.MetricsEventRequest{
+ User: &api.EventRequestUser{
+ Id: "18",
+ Email: "jk.rowling@harrypotter.com",
+ },
+ Value: 750,
+ },
+)
+```
+
+```csharp C#
+var user = new EventRequestUser {
+ Id = "18",
+ Email = "jk.rowling@harrypotter.com"
+};
+
+var request = new MetricsEventRequest {
+ User = user,
+ Value = 750
+};
+
+await trophy.Metrics.EventAsync("words-written", request);
+```
+
+```ruby Ruby
+result = client.metrics.event(
+ :key => 'words-written',
+ :user => {
+ :id => '18',
+ :email => 'jk.rowling@harrypotter.com'
+ },
+ :value => 750
+)
+```
+
+
From 40122da321d8d5fcff3b3e0d8c2d6f78aab10846 Mon Sep 17 00:00:00 2001
From: Charlie Brinicombe <55358291+cbrinicombe13@users.noreply.github.com>
Date: Sat, 6 Sep 2025 11:38:15 +0100
Subject: [PATCH 2/2] Changes from review
---
.prettierignore | 1 +
api-reference/idempotency.mdx | 14 +++----
platform/events.mdx | 10 ++++-
snippets/idempotent-event-tracking.mdx | 53 +++++++++++++-------------
4 files changed, 43 insertions(+), 35 deletions(-)
create mode 100644 .prettierignore
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..d8c0945
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+snippets/*
diff --git a/api-reference/idempotency.mdx b/api-reference/idempotency.mdx
index bcac58f..7f9d8f0 100644
--- a/api-reference/idempotency.mdx
+++ b/api-reference/idempotency.mdx
@@ -1,7 +1,7 @@
---
title: Idempotency
-description: Prevent unintended side-effects when retrying requests with idempotency controls built into the Trophy API and SDKs.
-"og:description": Prevent unintended side-effects when retrying requests with idempotency built into the Trophy API and SDKs using the `Idempotency-Key` request header.
+description: Prevent unintended side effects when retrying requests with idempotency controls built into the Trophy API and SDKs.
+"og:description": Prevent unintended side effects when retrying requests with idempotency built into the Trophy API and SDKs using the `Idempotency-Key` request header.
icon: repeat
---
@@ -22,9 +22,9 @@ To ensure idempotency is respected when sending events to Trophy, include an `Id
You can choose what to use as your idempotency key, but it should reflect the level of 'uniqueness' that you want Trophy to respect.
- For example, if you use Trophy to reward users for completing workouts, and
- want to make sure each user can only redeem rewards once for each workout, use
- the unique ID of the workout as your idempotency key.
+ For example, if you use Trophy to reward users for completing lessons, and
+ want to make sure each user can only redeem rewards once for each lesson, use
+ the unique ID of the lesson as your idempotency key.
Here's an example of what using an idempotency key looks like:
@@ -35,7 +35,7 @@ Here's an example of what using an idempotency key looks like:
When Trophy detects an idempotency key has been sent with an event, it will first check if the user the event relates to has used it before. It will then proceed to take one of the following actions:
-- If Trophy does recognise the user has used the idempotency key before, it will not process the event, returning a `202 Accepted` response. The response will reflect the state of the system when the original event that first used the idempotency key was sent.
+- If the user has used the idempotency key before, Trophy will not process the event, returning a `202 Accepted` response. The response will reflect the current state of the system, but will not increase the users metric total, complete any achievements, award any points, extend the streak, etc.
- If instead Trophy detects the user hasn't used the idempotency key before, it will process the event as usual, returning a `201 Created` response. Finally Trophy will store the idempotency key for lookup during any subsequent requests.
@@ -50,7 +50,7 @@ Additionally, when using an idempotency key the response will contain two proper
{
...,
"idempotentReplayed": true, // true if replayed, false otherwise
- "idempotencyKey": "test" // the original idempotency key if recognised, null otherwise
+ "idempotencyKey": "test" // the original idempotency key
}
```
diff --git a/platform/events.mdx b/platform/events.mdx
index 36e65c2..f2c7ca7 100644
--- a/platform/events.mdx
+++ b/platform/events.mdx
@@ -196,7 +196,15 @@ Watch Charlie integrate metric tracking into a simple NextJS application using t
### Idempotent Events
-Trophy supports preventing unintended side effects from duplicate events through an `Idempotency-Key` header. [Learn more about idempotency](/api-reference/idempotency).
+Trophy supports enforcing uniqueness on events so that users cannot increase a metric by taking the same exact action over and over.
+
+For example, a language learning app could specify that users can only increase the `lessons completed` metric by 1 for each unique lesson completed, so if they complete the same lesson twice only the first counts.
+
+This helps keep your codebase free of logic that checks if users have completed actions before, and can instead trust Trophy to uphold the constraints you need.
+
+To use idempotent events, use the `Idempotency-Key` header in the [metric event API](/api-reference/endpoints/metrics/send-a-metric-change-event).
+
+[Learn more about idempotency](/api-reference/idempotency).
## Get Support
diff --git a/snippets/idempotent-event-tracking.mdx b/snippets/idempotent-event-tracking.mdx
index 062721e..ac71d8e 100644
--- a/snippets/idempotent-event-tracking.mdx
+++ b/snippets/idempotent-event-tracking.mdx
@@ -1,8 +1,8 @@
```bash cURL {3}
-curl -X POST https://app.trophy.so/api/metrics/words-written/event \
+curl -X POST https://app.trophy.so/api/metrics/lessons-completed/event \
-H "X-API-KEY: " \
- -H "Idempotency-Key: " \
+ -H "Idempotency-Key: " \
-H "Content-Type: application/json" \
-d '{
"user": {
@@ -10,51 +10,50 @@ curl -X POST https://app.trophy.so/api/metrics/words-written/event \
"email": "user@example.com",
"tz": "Europe/London"
},
- "value": 750
+ "value": 1
}'
```
-```typescript Node {11-13}
-trophy.metrics.event(
- "words-written",
+```typescript Node {10-12}
+trophy.metrics.event("lessons-completed",
{
user: {
id: "18",
- email: "jk.rowling@harrypotter.com",
+ email: "user@example.com",
tz: "Europe/London",
},
- value: 750,
+ value: 1,
},
{
- idempotencyKey: "myIdempotencyKey",
+ idempotencyKey: "lesson-123",
}
);
```
```python Python
client.metrics.event(
- key="words-written",
+ key="lessons-completed",
user=EventRequestUser(
id="18",
- email="jk.rowling@harrypotter.com",
+ email="user@example.com",
tz="Europe/London",
),
- value=750.0,
+ value=1,
)
```
```php PHP
$user = new EventRequestUser([
'id' => '18',
- 'email' => 'jk.rowling@harrypotter.com'
+ 'email' => 'user@example.com'
]);
$request = new MetricsEventRequest([
'user' => $user,
- 'value' => 750
+ 'value' => 1
]);
-$trophy->metrics->event("words-written", $request);
+$trophy->metrics->event("lessons-completed", $request);
```
```java Java
@@ -62,25 +61,25 @@ MetricsEventRequest request = MetricsEventRequest.builder()
.user(
EventRequestUser.builder()
.id("18")
- .email("jk.rowling@harrypotter.com")
+ .email("user@example.com")
.build()
)
- .value(750)
+ .value(1)
.build();
-EventResponse response = client.metrics().event("words-written", request);
+EventResponse response = client.metrics().event("lessons-completed", request);
```
```go Go
response, err := client.Metrics.Event(
context.TODO(),
- "words-written",
+ "lessons-completed",
&api.MetricsEventRequest{
User: &api.EventRequestUser{
Id: "18",
- Email: "jk.rowling@harrypotter.com",
+ Email: "user@example.com",
},
- Value: 750,
+ Value: 1,
},
)
```
@@ -88,25 +87,25 @@ response, err := client.Metrics.Event(
```csharp C#
var user = new EventRequestUser {
Id = "18",
- Email = "jk.rowling@harrypotter.com"
+ Email = "user@example.com"
};
var request = new MetricsEventRequest {
User = user,
- Value = 750
+ Value = 1
};
-await trophy.Metrics.EventAsync("words-written", request);
+await trophy.Metrics.EventAsync("lessons-completed", request);
```
```ruby Ruby
result = client.metrics.event(
- :key => 'words-written',
+ :key => 'lessons-completed',
:user => {
:id => '18',
- :email => 'jk.rowling@harrypotter.com'
+ :email => 'user@example.com'
},
- :value => 750
+ :value => 1
)
```