Getting started
Use from any language
The REST API is plain HTTPS + JSON. Any language with an HTTP client can send events. Below is the same POST /v1/events call, written idiomatically in a handful of languages. Drop in your API key, run, done.
Every example targets:
text
POST https://api.recalled.dev/v1/events
Authorization: Bearer $RECALLED_API_KEY
Content-Type: application/jsonWith the body:
json
{
"action": "invoice.deleted",
"actor": { "id": "user_123", "email": "alice@example.com" },
"organization": "org_acme",
"targets": [{ "type": "invoice", "id": "inv_42" }],
"metadata": { "reason": "duplicate" }
}curl
bash
curl -X POST https://api.recalled.dev/v1/events \
-H "Authorization: Bearer $RECALLED_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "invoice.deleted",
"actor": { "id": "user_123", "email": "alice@example.com" },
"organization": "org_acme",
"targets": [{ "type": "invoice", "id": "inv_42" }],
"metadata": { "reason": "duplicate" }
}'Node.js (fetch, no SDK)
If you don't want the SDK and prefer raw fetch:
js
const response = await fetch("https://api.recalled.dev/v1/events", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RECALLED_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
action: "invoice.deleted",
actor: { id: "user_123", email: "alice@example.com" },
organization: "org_acme",
targets: [{ type: "invoice", id: "inv_42" }],
metadata: { reason: "duplicate" },
}),
});
if (!response.ok) throw new Error(await response.text());
const { data: event } = await response.json();Python (requests)
python
import os
import requests
response = requests.post(
"https://api.recalled.dev/v1/events",
headers={
"Authorization": f"Bearer {os.environ['RECALLED_API_KEY']}",
"Content-Type": "application/json",
},
json={
"action": "invoice.deleted",
"actor": {"id": "user_123", "email": "alice@example.com"},
"organization": "org_acme",
"targets": [{"type": "invoice", "id": "inv_42"}],
"metadata": {"reason": "duplicate"},
},
timeout=10,
)
response.raise_for_status()
event = response.json()["data"]Go (net/http)
go
package main
import (
"bytes"
"encoding/json"
"net/http"
"os"
"time"
)
func main() {
body, _ := json.Marshal(map[string]any{
"action": "invoice.deleted",
"actor": map[string]any{"id": "user_123", "email": "alice@example.com"},
"organization": "org_acme",
"targets": []map[string]any{{"type": "invoice", "id": "inv_42"}},
"metadata": map[string]any{"reason": "duplicate"},
})
req, _ := http.NewRequest("POST", "https://api.recalled.dev/v1/events", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("RECALLED_API_KEY"))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
}PHP (curl)
php
<?php
$body = json_encode([
"action" => "invoice.deleted",
"actor" => ["id" => "user_123", "email" => "alice@example.com"],
"organization" => "org_acme",
"targets" => [["type" => "invoice", "id" => "inv_42"]],
"metadata" => ["reason" => "duplicate"],
]);
$ch = curl_init("https://api.recalled.dev/v1/events");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("RECALLED_API_KEY"),
"Content-Type: application/json",
],
]);
$response = curl_exec($ch);
curl_close($ch);Ruby (Net::HTTP)
ruby
require "net/http"
require "json"
require "uri"
uri = URI("https://api.recalled.dev/v1/events")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV['RECALLED_API_KEY']}"
request["Content-Type"] = "application/json"
request.body = {
action: "invoice.deleted",
actor: { id: "user_123", email: "alice@example.com" },
organization: "org_acme",
targets: [{ type: "invoice", id: "inv_42" }],
metadata: { reason: "duplicate" },
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
endJava (java.net.http)
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
String body = """
{
"action": "invoice.deleted",
"actor": { "id": "user_123", "email": "alice@example.com" },
"organization": "org_acme",
"targets": [{ "type": "invoice", "id": "inv_42" }],
"metadata": { "reason": "duplicate" }
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.recalled.dev/v1/events"))
.header("Authorization", "Bearer " + System.getenv("RECALLED_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());Rust (reqwest)
rust
use serde_json::json;
let client = reqwest::Client::new();
let response = client
.post("https://api.recalled.dev/v1/events")
.header(
"Authorization",
format!("Bearer {}", std::env::var("RECALLED_API_KEY")?),
)
.header("Content-Type", "application/json")
.json(&json!({
"action": "invoice.deleted",
"actor": { "id": "user_123", "email": "alice@example.com" },
"organization": "org_acme",
"targets": [{ "type": "invoice", "id": "inv_42" }],
"metadata": { "reason": "duplicate" }
}))
.send()
.await?
.error_for_status()?;Error handling
Every non-2xx response has the same shape:
json
{
"error": {
"code": "PLAN_LIMIT_REACHED",
"message": "Monthly event quota exceeded",
"details": { "limit": 5000 }
}
}400:VALIDATION_ERROR, body failed schema (fields listed indetails)401:UNAUTHORIZED,INVALID_API_KEYorREVOKED_API_KEY, key missing, wrong or revoked403:FORBIDDEN, key is valid but the feature is plan-gated429:RATE_LIMITED(checkRateLimit-Reset) orPLAN_LIMIT_REACHED(monthly quota)5xx: retry with backoff
Full list in Error codes.
Retry strategy
Retry on 408, 429, 502, 503, 504 with exponential backoff (1s to 10min). Do not retry on 400, 401, 403, 404, those are permanent.
The npm SDK implements this for you, including a 24h in-memory queue for emit(). If you're on Node, use the SDK. Otherwise, wrap your HTTP client in a retry loop.