Select programming language for code examples

linkIdempotency

Some resources, such as webhook events may contain an idempotency token inside of a meta object. In the case of webhook events, these tokens allow you to safely retry events without accidentally responding to the same event multiple times.

linkExample scenario

When a user creates a new license, you would want to make sure that the license.created webhook event that you're listening for would only be acted upon once, regardless of how many times you retry the event, to guarantee you only charge your user for a single license.

You can accomplish this by logging the idempotency token (to a database, for example, Redis) and ignoring future webhook events that come through with an identical token, signaling a retried event.

Although retrying a webhook event creates a new resource, the idempotency token will stay the same throughout the event's lifetime.

Example resource

A failed webhook event resource.

{
"data": {
"id": "2bc99fe1-f315-4877-ac5f-542240c4e883",
"type": "webhook-events",
"meta": {
"idempotencyToken": "e8e0fbb598e8bcfd0e94ceb79199edc79e6ab53f4a4bbb32d7aede7964e7c3v2",
},
"links": {
"self": "/v1/accounts/{ACCOUNT}/webhook-events/2bc99fe1-f315-4877-ac5f-542240c4e883"
},
"attributes": {
"endpoint": "https://example.com/webhooks",
"payload": "{\"data\":{…}}",
"event": "license.created",
"status": "failed",
"lastResponseCode": 500,
"lastResponseBody": "Internal Server Error",
"created": "2017-01-02T20:26:53.464Z",
"updated": "2017-01-02T20:26:53.464Z"
},
"relationships": {
"account": {
"links": {
"related": "/v1/accounts/{ACCOUNT}"
},
"data": {
"type": "accounts",
"id": "{ACCOUNT}"
}
}
}
}
}

Example request

Retry the above failed webhook event resource.

const fetch = require("node-fetch")
 
const response = await fetch("https://api.keygen.sh/v1/accounts/{ACCOUNT}/webhook-events/2bc99fe1-f315-4877-ac5f-542240c4e883/actions/retry", {
method: "POST",
headers: {
"Accept": "application/vnd.api+json",
"Authorization": "Bearer {TOKEN}"
}
})
 
const { data, errors } = await response.json()
import requests
import json
 
res = requests.post(
"https://api.keygen.sh/v1/accounts/{ACCOUNT}/webhook-events/2bc99fe1-f315-4877-ac5f-542240c4e883/actions/retry",
headers={
"Accept": "application/vnd.api+json",
"Authorization": "Bearer {TOKEN}"
}
).json()
import SwiftyJSON
import Alamofire
 
Alamofire.request("https://api.keygen.sh/v1/accounts/{ACCOUNT}/webhook-events/2bc99fe1-f315-4877-ac5f-542240c4e883/actions/retry",
method: .post,
headers: [
"Accept": "application/vnd.api+json",
"Authorization": "Bearer {TOKEN}"
]
).responseJSON { response in
let json = JSON(data: response.data!)
}
using RestSharp;
 
var client = new RestClient("https://api.keygen.sh/v1/accounts/{ACCOUNT}");
var request = new RestRequest(
"webhook-events/2bc99fe1-f315-4877-ac5f-542240c4e883/actions/retry",
Method.POST
);
 
request.AddHeader("Accept", "application/vnd.api+json");
request.AddHeader("Authorization", "Bearer {TOKEN}");
 
var response = client.Execute(request);
import com.mashape.unirest.http.exceptions.*
import com.mashape.unirest.http.*
 
val res = Unirest.post("https://api.keygen.sh/v1/accounts/{ACCOUNT}/webhook-events/2bc99fe1-f315-4877-ac5f-542240c4e883/actions/retry")
.header("Authorization", "Bearer {TOKEN}")
.header("Accept", "application/vnd.api+json")
.asJson()
import com.mashape.unirest.http.exceptions.*;
import com.mashape.unirest.http.*;
 
HttpResponse<JsonNode> res = Unirest.post("https://api.keygen.sh/v1/accounts/{ACCOUNT}/webhook-events/2bc99fe1-f315-4877-ac5f-542240c4e883/actions/retry")
.header("Authorization", "Bearer {TOKEN}")
.header("Accept", "application/vnd.api+json")
.asJson();
#include <iostream>
#include <string>
#include <cpprest/http_client.h>
#include <cpprest/filestream.h>
 
using namespace std;
using namespace web;
using namespace web::http;
using namespace web::http::client;
using namespace utility;
 
http_client client("https://api.keygen.sh/v1/accounts/{ACCOUNT}");
http_request req;
 
req.headers().add("Authorization", "Bearer {TOKEN}");
req.headers().add("Accept", "application/json");
 
req.set_request_uri("/webhook-events/2bc99fe1-f315-4877-ac5f-542240c4e883/actions/retry");
req.set_method(methods::POST);
 
client.request(req)
.then([](http_response res) {
auto data = res.extract_json().get();
})
.wait();
curl -X POST https://api.keygen.sh/v1/accounts/{ACCOUNT}/webhook-events/2bc99fe1-f315-4877-ac5f-542240c4e883/actions/retry \
-H 'Accept: application/vnd.api+json' \
-H 'Authorization: Bearer {TOKEN}'

Example response

Notice that it's a new resource, yet the idempotency token matches the original event.

{
"data": {
"id": "1bda5fd5-6d82-49f5-b5b8-71bb432a32bb",
"type": "webhook-events",
"meta": {
"idempotencyToken": "e8e0fbb598e8bcfd0e94ceb79199edc79e6ab53f4a4bbb32d7aede7964e7c3v2",
},
"links": {
"self": "/v1/accounts/{ACCOUNT}/webhook-events/1bda5fd5-6d82-49f5-b5b8-71bb432a32bb"
},
"attributes": {
"endpoint": "https://example.com/webhooks",
"payload": "{\"data\":{…}}",
"event": "license.created",
"status": "queued",
"lastResponseCode": null,
"lastResponseBody": null,
"created": "2017-01-02T20:26:53.464Z",
"updated": "2017-01-02T20:26:53.464Z"
},
"relationships": {
"account": {
"links": {
"related": "/v1/accounts/{ACCOUNT}"
},
"data": {
"type": "accounts",
"id": "{ACCOUNT}"
}
}
}
}
}