Webhooks
Get notified the moment something happens, instead of polling for it.
Events
invoice.created,
invoice.sent,
invoice.paid,
invoice.canceled. Each delivery's body carries
the full invoice record under data.
{ "event": "invoice.paid", "created_at": "2026-07-30T17:45:26.525Z", "data": { "_id": "…", "invoice_number": "IPP/1031", "status": "Paid" /* … */ } }
Verifying a delivery
Every request carries an X-Biflus-Signature
header: sha256=<hex>, an HMAC-SHA256 of
the raw request body using your webhook's secret.
Recompute it on your end and compare before trusting the payload. An
X-Biflus-Event header carries the event type too,
so you can route without parsing the body first.
// Node.js const crypto = require('crypto'); const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); const valid = expected === req.headers['x-biflus-signature'];
Delivery and retries
A 2xx response marks a delivery successful.
Anything else (including a timeout, 10s) is retried automatically with
backoff: 1 minute, 5 minutes, 30 minutes, then 2 hours, up to 5 attempts
total, after which it's marked dead and given up on.
Fields you can set
Three settable fields. A subscription also returns its signing secret, which you'll need to verify deliveries.
urlstringRequiredMust be http(s). Where deliveries are POSTed.
eventsarrayRequiredAt least one, from the list above.
activebooleanDefaults to true on create. Set false to pause without deleting.
Read-only fields
Set by the system when the subscription is created.
_idstring (id)The subscription's public id, e.g. wh_3fa8c2.
secretstringGenerated once, at creation. Every delivery is signed with it (X-Biflus-Signature: sha256=…) — use it to verify a request really came from Biflus. Store it when you create the subscription.
companystring (id)Your company — scoping is automatic.
Created Date, Modified DatedateISO timestamps maintained automatically.
- Store
secretthe moment you create a webhook —GETnever returns it again, and there's no "regenerate" endpoint, only delete-and-recreate. - Respond
2xximmediately and do slow work asynchronously on your side — anything over 10 seconds is treated as a failure and queued for retry. - Use
active: falsefor a planned pause (a maintenance window); delete the webhook instead for a permanent stop, so it doesn't linger in your list.
- Don't skip signature verification, even in development. Anyone who learns your endpoint URL can POST a fake payload — checking
X-Biflus-Signatureis the only way to know a delivery is really from Biflus. - Don't expect a paused webhook to replay missed events. Nothing sent while
activeis false is queued — it's simply not sent, and there's no backfill when you turn it back on. - Don't treat a 5th failed attempt as recoverable. Once a delivery is marked
dead, it stops retrying entirely — if your endpoint was down, you'll need to re-fetch the affected invoices directly, not wait for a 6th attempt.
List webhooks
secret is only ever returned once, on creation. It's never included here.
curl https://api.biflus.com/v1/webhooks \ -H "Authorization: Bearer sk_test_YOUR_API_KEY"
import requests
resp = requests.get(
"https://api.biflus.com/v1/webhooks",
headers={"Authorization": "Bearer sk_test_YOUR_API_KEY"},
)
print(resp.json())const res = await fetch("https://api.biflus.com/v1/webhooks", {
headers: { "Authorization": "Bearer sk_test_YOUR_API_KEY" }
});
const data = await res.json();using System.Net.Http;
using System.Net.Http.Headers;
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.biflus.com/v1/webhooks");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "sk_test_YOUR_API_KEY");
var response = await client.SendAsync(request);
var data = await response.Content.ReadAsStringAsync();package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.biflus.com/v1/webhooks", nil)
req.Header.Set("Authorization", "Bearer sk_test_YOUR_API_KEY")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
fmt.Println(string(data))
}import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.biflus.com/v1/webhooks"))
.header("Authorization", "Bearer sk_test_YOUR_API_KEY")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); require 'net/http'
require 'json'
uri = URI("https://api.biflus.com/v1/webhooks")
req = Net::HTTP::Get.new(uri)
req['Authorization'] = 'Bearer sk_test_YOUR_API_KEY'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body){ "data": [ { "_id": "wh_3fa8c2", "url": "https://yourapp.com/webhooks/biflus", "events": [ "invoice.paid", "invoice.sent" ], "active": true } ], "pagination": { "count": 1, "remaining": 0, "next_cursor": null } }
Get a webhook
idpath param, stringRequiredThe webhook's _id.
curl https://api.biflus.com/v1/webhooks/wh_3fa8c2 \ -H "Authorization: Bearer sk_test_YOUR_API_KEY"
import requests
resp = requests.get(
"https://api.biflus.com/v1/webhooks/wh_3fa8c2",
headers={"Authorization": "Bearer sk_test_YOUR_API_KEY"},
)
print(resp.json())const res = await fetch("https://api.biflus.com/v1/webhooks/wh_3fa8c2", {
headers: { "Authorization": "Bearer sk_test_YOUR_API_KEY" }
});
const data = await res.json();using System.Net.Http;
using System.Net.Http.Headers;
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.biflus.com/v1/webhooks/wh_3fa8c2");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "sk_test_YOUR_API_KEY");
var response = await client.SendAsync(request);
var data = await response.Content.ReadAsStringAsync();package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.biflus.com/v1/webhooks/wh_3fa8c2", nil)
req.Header.Set("Authorization", "Bearer sk_test_YOUR_API_KEY")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
fmt.Println(string(data))
}import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.biflus.com/v1/webhooks/wh_3fa8c2"))
.header("Authorization", "Bearer sk_test_YOUR_API_KEY")
.method("GET", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); require 'net/http'
require 'json'
uri = URI("https://api.biflus.com/v1/webhooks/wh_3fa8c2")
req = Net::HTTP::Get.new(uri)
req['Authorization'] = 'Bearer sk_test_YOUR_API_KEY'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body){ "data": { "_id": "wh_3fa8c2", "url": "https://yourapp.com/webhooks/biflus", "events": [ "invoice.paid", "invoice.sent" ], "active": true } }
Create a webhook
Save secret from the response: it's how you verify deliveries are really from Biflus, and it's never shown again.
curl -X POST https://api.biflus.com/v1/webhooks \
-H "Authorization: Bearer sk_test_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://yourapp.com/webhooks/biflus", "events": ["invoice.paid", "invoice.sent"]}'import requests
resp = requests.post(
"https://api.biflus.com/v1/webhooks",
headers={"Authorization": "Bearer sk_test_YOUR_API_KEY"},
json={"url": "https://yourapp.com/webhooks/biflus", "events": ["invoice.paid", "invoice.sent"]},
)
print(resp.json())const res = await fetch("https://api.biflus.com/v1/webhooks", {
method: "POST",
headers: {
"Authorization": "Bearer sk_test_YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
url: "https://yourapp.com/webhooks/biflus",
events: ["invoice.paid", "invoice.sent"]
})
});
const data = await res.json();using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.biflus.com/v1/webhooks");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "sk_test_YOUR_API_KEY");
request.Content = new StringContent("{"url": "https://yourapp.com/webhooks/biflus", "events": ["invoice.paid", "invoice.sent"]}", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var data = await response.Content.ReadAsStringAsync();package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"url": "https://yourapp.com/webhooks/biflus", "events": ["invoice.paid", "invoice.sent"]}`)
req, _ := http.NewRequest("POST", "https://api.biflus.com/v1/webhooks", body)
req.Header.Set("Authorization", "Bearer sk_test_YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
fmt.Println(string(data))
}import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.biflus.com/v1/webhooks"))
.header("Authorization", "Bearer sk_test_YOUR_API_KEY")
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("{"url": "https://yourapp.com/webhooks/biflus", "events": ["invoice.paid", "invoice.sent"]}"))
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); require 'net/http'
require 'json'
uri = URI("https://api.biflus.com/v1/webhooks")
req = Net::HTTP::Post.new(uri)
req['Authorization'] = 'Bearer sk_test_YOUR_API_KEY'
req['Content-Type'] = 'application/json'
req.body = '{"url": "https://yourapp.com/webhooks/biflus", "events": ["invoice.paid", "invoice.sent"]}'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body){ "data": { "_id": "wh_3fa8c2", "url": "https://yourapp.com/webhooks/biflus", "events": [ "invoice.paid", "invoice.sent" ], "active": true, "secret": "a3f8c1…" } }
Update a webhook
Paused webhooks stop receiving deliveries immediately, and pick back up on their normal events once active is set back to true. Nothing sent while paused is queued or replayed.
curl -X PATCH https://api.biflus.com/v1/webhooks/wh_3fa8c2 \
-H "Authorization: Bearer sk_test_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"active": false}'import requests
resp = requests.patch(
"https://api.biflus.com/v1/webhooks/wh_3fa8c2",
headers={"Authorization": "Bearer sk_test_YOUR_API_KEY"},
json={"active": False},
)
print(resp.json())const res = await fetch("https://api.biflus.com/v1/webhooks/wh_3fa8c2", {
method: "PATCH",
headers: {
"Authorization": "Bearer sk_test_YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({ active: false })
});
const data = await res.json();using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Patch, "https://api.biflus.com/v1/webhooks/wh_3fa8c2");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "sk_test_YOUR_API_KEY");
request.Content = new StringContent("{"active": false}", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
var data = await response.Content.ReadAsStringAsync();package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"active": false}`)
req, _ := http.NewRequest("PATCH", "https://api.biflus.com/v1/webhooks/wh_3fa8c2", body)
req.Header.Set("Authorization", "Bearer sk_test_YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
fmt.Println(string(data))
}import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.biflus.com/v1/webhooks/wh_3fa8c2"))
.header("Authorization", "Bearer sk_test_YOUR_API_KEY")
.header("Content-Type", "application/json")
.method("PATCH", HttpRequest.BodyPublishers.ofString("{"active": false}"))
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); require 'net/http'
require 'json'
uri = URI("https://api.biflus.com/v1/webhooks/wh_3fa8c2")
req = Net::HTTP::Patch.new(uri)
req['Authorization'] = 'Bearer sk_test_YOUR_API_KEY'
req['Content-Type'] = 'application/json'
req.body = '{"active": false}'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body){ "data": { "_id": "wh_3fa8c2", "url": "https://yourapp.com/webhooks/biflus", "events": [ "invoice.paid", "invoice.sent" ], "active": false } }
Delete a webhook
Permanent — deliveries already queued for retry are abandoned along with it.
curl -X DELETE https://api.biflus.com/v1/webhooks/wh_3fa8c2 \ -H "Authorization: Bearer sk_test_YOUR_API_KEY"
import requests
resp = requests.delete(
"https://api.biflus.com/v1/webhooks/wh_3fa8c2",
headers={"Authorization": "Bearer sk_test_YOUR_API_KEY"},
)
print(resp.json())const res = await fetch("https://api.biflus.com/v1/webhooks/wh_3fa8c2", {
method: "DELETE",
headers: { "Authorization": "Bearer sk_test_YOUR_API_KEY" }
});
const data = await res.json();using System.Net.Http;
using System.Net.Http.Headers;
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Delete, "https://api.biflus.com/v1/webhooks/wh_3fa8c2");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "sk_test_YOUR_API_KEY");
var response = await client.SendAsync(request);
var data = await response.Content.ReadAsStringAsync();package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("DELETE", "https://api.biflus.com/v1/webhooks/wh_3fa8c2", nil)
req.Header.Set("Authorization", "Bearer sk_test_YOUR_API_KEY")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
fmt.Println(string(data))
}import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.biflus.com/v1/webhooks/wh_3fa8c2"))
.header("Authorization", "Bearer sk_test_YOUR_API_KEY")
.method("DELETE", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); require 'net/http'
require 'json'
uri = URI("https://api.biflus.com/v1/webhooks/wh_3fa8c2")
req = Net::HTTP::Delete.new(uri)
req['Authorization'] = 'Bearer sk_test_YOUR_API_KEY'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body){ "deleted": true, "id": "wh_3fa8c2" }