docs/ Quickstart
Send your first webhook in five minutes.
Install an SDK, create an endpoint, send a signed event and verify it on the other side. Everything here works with a free test key.
- Updated
- Oct 8, 2026
- API version
- 2026-09-01
- Time
- about 5 min
Language
Step 01
Install an SDK
Forge has official SDKs for eight languages; this guide uses Node.js, Python and Go, or plain curl. Pick a language once with the tabs: every example on this page switches with it, and we remember your choice for next time.
# Nothing to install. Any curl from the last five years works.
curl --version
npm install @forgehq/node
pip install forgehq
go get github.com/forgehq/forge-go/v4
Other languages and tools are on the SDKs page.
Step 02
Authenticate
Create a key on the sign-up page and export it. Test keys start with fk_test_ and live keys with fk_live_; the two never see each other’s data.
export FORGE_KEY="fk_test_4eC39HqLyjWDarjtT1zdp7dc"
curl https://api.forge.example/v1/ping \
-H "Authorization: Bearer $FORGE_KEY"
import { Forge } from "@forgehq/node";
const forge = new Forge(process.env.FORGE_KEY);
console.log(await forge.ping()); // { ok: true, mode: "test" }
import os
from forge import Forge
forge = Forge(os.environ["FORGE_KEY"])
print(forge.ping()) # {"ok": True, "mode": "test"}
client := forge.NewClient(os.Getenv("FORGE_KEY"))
pong, err := client.Ping(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(pong.Mode) // test
Step 03
Create an endpoint
An endpoint is a URL that receives events, usually owned by one of your customers. Subscribe it to event types with wildcards. Each endpoint gets its own signing secret, returned once.
curl https://api.forge.example/v1/endpoints \
-H "Authorization: Bearer $FORGE_KEY" \
-d '{
"tenant": "acme-co",
"url": "https://hooks.acme.example/forge",
"filter_types": ["invoice.*"]
}'
const endpoint = await forge.endpoints.create({
tenant: "acme-co",
url: "https://hooks.acme.example/forge",
filterTypes: ["invoice.*"],
});
console.log(endpoint.secret); // whsec_...
endpoint = forge.endpoints.create(
tenant="acme-co",
url="https://hooks.acme.example/forge",
filter_types=["invoice.*"],
)
print(endpoint.secret) # whsec_...
endpoint, err := client.Endpoints.Create(ctx, &forge.EndpointParams{
Tenant: "acme-co",
URL: "https://hooks.acme.example/forge",
FilterTypes: []string{"invoice.*"},
})
{
"id": "ep_7Hc2Qm",
"object": "endpoint",
"tenant": "acme-co",
"url": "https://hooks.acme.example/forge",
"filter_types": ["invoice.*"],
"secret": "whsec_MfKQ9r8GkjVsXmNn2pRq",
"status": "enabled"
}
Step 04
Send an event
Send each event once, with the tenant it belongs to. Forge answers 202 as soon as the event is stored in three zones, then delivers it to every matching endpoint. Pass an idempotency key so a retry on your side can never send it twice.
curl https://api.forge.example/v1/events \
-H "Authorization: Bearer $FORGE_KEY" \
-H "Idempotency-Key: ord_8812-paid" \
-d '{
"type": "invoice.paid",
"tenant": "acme-co",
"data": { "invoice_id": "in_8812", "amount": 4900 }
}'
await forge.events.send({
type: "invoice.paid",
tenant: "acme-co",
data: { invoice_id: "in_8812", amount: 4900 },
}, { idempotencyKey: "ord_8812-paid" });
forge.events.send(
type="invoice.paid",
tenant="acme-co",
data={"invoice_id": "in_8812", "amount": 4900},
idempotency_key="ord_8812-paid",
)
_, err = client.Events.Send(ctx, &forge.EventParams{
Type: "invoice.paid",
Tenant: "acme-co",
Data: map[string]any{"invoice_id": "in_8812", "amount": 4900},
IdempotencyKey: "ord_8812-paid",
})
Step 05
Verify signatures
On the receiving side, check every request before trusting it. The signature covers the message id, timestamp and the raw body, so verify before your framework parses JSON. Requests older than five minutes are rejected to stop replays.
# Manual check with openssl: sign "{id}.{timestamp}.{body}" with the endpoint secret
signed="$FORGE_ID.$FORGE_TIMESTAMP.$BODY"
echo -n "$signed" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64
# Compare with the value after "v1," in the forge-signature header
import express from "express";
import { verifyWebhook } from "@forgehq/node";
const app = express();
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = verifyWebhook(req.body, req.headers, process.env.FORGE_SECRET);
console.log("received", event.type);
res.sendStatus(204);
} catch (err) {
res.sendStatus(400); // bad or stale signature
}
});
from flask import Flask, request
from forge import verify_webhook, InvalidSignature
app = Flask(__name__)
@app.post("/webhooks")
def webhooks():
try:
event = verify_webhook(request.get_data(), request.headers, FORGE_SECRET)
except InvalidSignature:
return "", 400
print("received", event.type)
return "", 204
http.HandleFunc("/webhooks", func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
event, err := forge.VerifyWebhook(body, r.Header, os.Getenv("FORGE_SECRET"))
if err != nil {
http.Error(w, "bad signature", http.StatusBadRequest)
return
}
log.Println("received", event.Type)
w.WriteHeader(http.StatusNoContent)
})
Step 06
Test locally
The Forge CLI opens a tunnel to your machine and forwards test events to a local port, so you can develop without deploying. Trigger sample events from a second terminal.
brew install forgehq/tap/forge
forge login
forge listen --forward localhost:3000/webhooks
forge trigger invoice.paid --tenant acme-co