B
Biflus Docs
biflus.com Get API key

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.

urlstringRequired

Must be http(s). Where deliveries are POSTed.

eventsarrayRequired

At least one, from the list above.

activeboolean

Defaults 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.

secretstring

Generated 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 Datedate

ISO timestamps maintained automatically.

Best practices
  • Store secret the moment you create a webhook — GET never returns it again, and there's no "regenerate" endpoint, only delete-and-recreate.
  • Respond 2xx immediately and do slow work asynchronously on your side — anything over 10 seconds is treated as a failure and queued for retry.
  • Use active: false for a planned pause (a maintenance window); delete the webhook instead for a permanent stop, so it doesn't linger in your list.
Common mistakes
  • Don't skip signature verification, even in development. Anyone who learns your endpoint URL can POST a fake payload — checking X-Biflus-Signature is the only way to know a delivery is really from Biflus.
  • Don't expect a paused webhook to replay missed events. Nothing sent while active is 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

GET/v1/webhooks available Paginated

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)
Response
{
  "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

GET/v1/webhooks/:id available Fetch one webhook
idpath param, stringRequired

The 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)
Response
{
  "data": {
    "_id": "wh_3fa8c2",
    "url": "https://yourapp.com/webhooks/biflus",
    "events": [
      "invoice.paid",
      "invoice.sent"
    ],
    "active": true
  }
}

Create a webhook

POST/v1/webhooks available 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)
Response
{
  "data": {
    "_id": "wh_3fa8c2",
    "url": "https://yourapp.com/webhooks/biflus",
    "events": [
      "invoice.paid",
      "invoice.sent"
    ],
    "active": true,
    "secret": "a3f8c1…"
  }
}

Update a webhook

PATCH/v1/webhooks/:id available Update url, events, or pause with active: false

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)
Response
{
  "data": {
    "_id": "wh_3fa8c2",
    "url": "https://yourapp.com/webhooks/biflus",
    "events": [
      "invoice.paid",
      "invoice.sent"
    ],
    "active": false
  }
}

Delete a webhook

DELETE/v1/webhooks/:id available Remove 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)
Response
{
  "deleted": true,
  "id": "wh_3fa8c2"
}
This page is updated as the API changes. If something here doesn't match what you're seeing, that's a bug worth flagging to the Biflus team.