recalled.dev
Démarrage

Utiliser depuis n'importe quel langage

L'API REST, c'est du HTTPS + JSON pur. N'importe quel langage avec un client HTTP peut envoyer des events. Ci-dessous, le même appel POST /v1/events écrit de manière idiomatique dans plusieurs langages. Insère ta clé API, lance, terminé.

Chaque exemple cible :

text
POST https://api.recalled.dev/v1/events
Authorization: Bearer $RECALLED_API_KEY
Content-Type: application/json

Avec le 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, sans SDK)

Si tu ne veux pas du SDK et préfères un fetch brut :

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)
end

Java (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()?;

Gestion des erreurs

Toutes les réponses non-2xx ont la même forme :

json
{
  "error": {
    "code": "PLAN_LIMIT_REACHED",
    "message": "Monthly event quota exceeded",
    "details": { "limit": 5000 }
  }
}
  • 400 : VALIDATION_ERROR, le body a échoué la validation (les champs fautifs sont dans details)
  • 401 : UNAUTHORIZED, INVALID_API_KEY ou REVOKED_API_KEY, clé manquante, invalide ou révoquée
  • 403 : FORBIDDEN, la clé est valide mais la feature est gated par le plan
  • 429 : RATE_LIMITED (regarde RateLimit-Reset) ou PLAN_LIMIT_REACHED (quota mensuel)
  • 5xx : retry avec backoff

Liste complète dans Codes d'erreur.

Stratégie de retry

Retry sur 408, 429, 502, 503, 504 avec backoff exponentiel (1s à 10min). Pas de retry sur 400, 401, 403, 404, ce sont des erreurs permanentes.

Le SDK npm implémente tout ça pour toi, y compris une queue en mémoire de 24 h pour emit(). Si tu es sur Node, utilise le SDK. Sinon, enrobe ton client HTTP dans une boucle de retry.