Like a boomerang — you throw something out and it comes back to you.
You POST a request. Boomerang returns a 202 in under 50 ms. Your handler runs in the background. When it's done, the result flies back to whatever URL you gave it. That's it.
No polling loops. No managing background threads. No reinventing the webhook wheel.
Client → POST /jobs { callbackUrl }
↓ 202 + jobId (< 50 ms)
[background]
↓ your handler runs
↓ POST callbackUrl { jobId, status, result }
The server is a standalone Spring Boot service (or embeddable starter) backed by Redis. Thin SDKs in every language handle the HTTP calls — no queue logic, no Redis dependency on the client side.
Pick the SDK for your stack. All of them speak the same wire format and honour the same contract.
<dependency>
<groupId>io.github.sameerchereddy</groupId>
<artifactId>boomerang-starter</artifactId>
<version>VERSION</version>
</dependency>@SpringBootApplication
@EnableBoomerang
public class MyApp { ... }
@Component
public class MyHandler {
@BoomerangHandler
public Map<String, Object> handle(SyncContext ctx) {
// do the work, return the result
return Map.of("url", generate(ctx.getPayload()));
}
}See boomerang-starter for full configuration reference.
npm install @sameerchereddy/boomerang-clientimport { BoomerangClient } from '@sameerchereddy/boomerang-client';
const client = new BoomerangClient({ baseUrl: 'https://boomerang.your-org.com', token: '<jwt>' });
const { jobId } = await client.trigger({ callbackUrl: 'https://yourapp.com/hooks/done' });
const status = await client.poll(jobId);See boomerang-node/README.md for webhook middleware for Express and Fastify.
go get github.com/sameerchereddy/boomerang-go@v1.0.1client := boomerang.NewClient("https://boomerang.your-org.com", "<jwt>")
resp, _ := client.Trigger(ctx, &boomerang.TriggerRequest{CallbackUrl: "https://yourapp.com/hooks/done"})
status, _ := client.Poll(ctx, resp.JobId)See boomerang-go/README.md for full documentation.
Built by @sudheerr48.
pip install boomerang-pythonfrom boomerang import BoomerangClient
client = BoomerangClient(base_url="https://boomerang.your-org.com", token="<jwt>")
job = client.trigger(callback_url="https://yourapp.com/hooks/done")
status = client.poll(job.job_id)See boomerang-python/README.md for full documentation.
Built by @agoginen.
dotnet add package Boomerang.Client
dotnet add package Boomerang.Client.AspNetCore # optional — webhook filter for ASP.NET Corevar client = new BoomerangClient(new BoomerangClientOptions
{
BaseUrl = new Uri("https://boomerang.your-org.com/"),
Token = Environment.GetEnvironmentVariable("BOOMERANG_JWT")!,
});
var job = await client.TriggerAsync(new BoomerangTriggerRequest
{
CallbackUrl = "https://yourapp.com/hooks/done",
CallbackSecret = Environment.GetEnvironmentVariable("WEBHOOK_SECRET"),
});
var status = await client.PollAsync(job.JobId);// ASP.NET Core — signature verified automatically before the action runs
[HttpPost("/hooks/done")]
[BoomerangWebhook(SecretEnvironmentVariable = "WEBHOOK_SECRET")]
public IActionResult OnDone([FromBody] BoomerangWebhookPayload payload) => Ok();See boomerang-dotnet/README.md for full documentation.
All endpoints require Authorization: Bearer <jwt> (HS256). Paths are relative to boomerang.base-path (default /sync).
| Method | Path | Description |
|---|---|---|
POST |
/{base-path} |
Enqueue a job — returns 202 { jobId } |
GET |
/{base-path}/{jobId} |
Poll job status (PENDING, IN_PROGRESS, DONE, FAILED) |
GET |
/{base-path}/failed-webhooks |
List dead-lettered webhook deliveries |
POST |
/{base-path}/failed-webhooks/{jobId}/replay |
Re-attempt a failed delivery |
DELETE |
/{base-path}/failed-webhooks/{jobId} |
Discard a failed delivery |
| Field | Type | Required | Description |
|---|---|---|---|
callbackUrl |
string | Yes | URL to POST the result to when the job completes |
callbackSecret |
string | No | Min 32 chars. Enables X-Signature-SHA256 on callbacks |
idempotencyKey |
string | No | Max 128 chars. Duplicate within cooldown window returns 409 |
payload |
object | No | Arbitrary JSON passed to your handler |
messageVersion |
string | No | Schema version string (e.g. "v1") for payload evolution |
workerUrl |
string | Standalone only | HTTP URL Boomerang POSTs to instead of an in-process handler |
{
"boomerangVersion": "1",
"jobId": "a1b2c3...",
"status": "DONE",
"result": { ... },
"completedAt": "2026-03-22T10:15:30Z",
"error": null
}When callbackSecret is set, every callback includes X-Signature-SHA256: sha256=<lowercase hex> — HMAC-SHA256 over the raw request body. All SDKs include a helper to verify this in constant time.
docker compose -f boomerang-standalone/docker-compose.yml upThe server starts on port 8080. Set BOOMERANG_JWT_SECRET (min 32 chars) and SPRING_DATA_REDIS_HOST in the compose file or via environment variables.
Boomerang does not issue JWTs — generate one with jwt-cli:
JWT=$(jwt encode --secret "your-secret-min-32-chars!!" --sub myapp)
curl -X POST http://localhost:8080/sync \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"callbackUrl":"https://yourapp.com/hooks/done","workerUrl":"https://yourapp.com/internal/work"}'Apache 2.0
