From bccb381bfa7f41c153da0256603ed4cdfc5f323b Mon Sep 17 00:00:00 2001 From: Mayuresh Date: Wed, 1 Apr 2026 17:19:37 +0530 Subject: [PATCH] scripts to run benchmarks --- benchmarks/README.md | 157 + benchmarks/e2e/bench_test.go | 186 + benchmarks/e2e/keys_test.go | 13 + benchmarks/e2e/mocks_test.go | 63 + benchmarks/e2e/setup_test.go | 466 +++ benchmarks/e2e/testdata/beckn.yaml | 3380 +++++++++++++++++ benchmarks/e2e/testdata/confirm_request.json | 84 + benchmarks/e2e/testdata/discover_request.json | 17 + benchmarks/e2e/testdata/init_request.json | 80 + .../e2e/testdata/routing-BAPCaller.yaml | 13 + benchmarks/e2e/testdata/select_request.json | 55 + benchmarks/run_benchmarks.sh | 145 + benchmarks/tools/parse_results.go | 258 ++ 13 files changed, 4917 insertions(+) create mode 100644 benchmarks/README.md create mode 100644 benchmarks/e2e/bench_test.go create mode 100644 benchmarks/e2e/keys_test.go create mode 100644 benchmarks/e2e/mocks_test.go create mode 100644 benchmarks/e2e/setup_test.go create mode 100644 benchmarks/e2e/testdata/beckn.yaml create mode 100644 benchmarks/e2e/testdata/confirm_request.json create mode 100644 benchmarks/e2e/testdata/discover_request.json create mode 100644 benchmarks/e2e/testdata/init_request.json create mode 100644 benchmarks/e2e/testdata/routing-BAPCaller.yaml create mode 100644 benchmarks/e2e/testdata/select_request.json create mode 100755 benchmarks/run_benchmarks.sh create mode 100644 benchmarks/tools/parse_results.go diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..6d0615b --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,157 @@ +# beckn-onix Adapter Benchmarks + +End-to-end performance benchmarks for the beckn-onix ONIX adapter, using Go's native `testing.B` framework and `net/http/httptest`. No Docker, no external services — everything runs in-process. + +--- + +## Quick Start + +```bash +# From the repo root +go mod tidy # fetch miniredis + benchstat checksums +bash benchmarks/run_benchmarks.sh # compile plugins, run all scenarios, generate report +``` + +Results land in `benchmarks/results//`. + +--- + +## What Is Being Benchmarked + +The benchmarks target the **`bapTxnCaller`** handler — the primary outbound path a BAP takes when initiating a Beckn transaction. Every request travels through the full production pipeline: + +``` +Benchmark goroutine(s) + │ HTTP POST /bap/caller/ + ▼ +httptest.Server ← ONIX adapter (real compiled .so plugins) + │ + ├── addRoute router plugin resolve BPP URL from routing config + ├── sign signer + simplekeymanager Ed25519 / BLAKE-512 signing + └── validateSchema schemav2validator Beckn OpenAPI spec validation + │ + └──▶ httptest mock BPP (instant ACK — no network) +``` + +Mock services replace all external dependencies so results reflect **adapter-internal latency only**: + +| Dependency | Replaced by | +|------------|-------------| +| Redis | `miniredis` (in-process) | +| BPP backend | `httptest` mock — returns `{"message":{"ack":{"status":"ACK"}}}` | +| Beckn registry | `httptest` mock — returns the dev key pair for signature verification | + +--- + +## Benchmark Scenarios + +| Benchmark | What it measures | +|-----------|-----------------| +| `BenchmarkBAPCaller_Discover` | Baseline single-goroutine latency for `/discover` | +| `BenchmarkBAPCaller_Discover_Parallel` | Throughput under concurrent load; run with `-cpu=1,2,4,8,16` | +| `BenchmarkBAPCaller_AllActions` | Per-action latency: `discover`, `select`, `init`, `confirm` | +| `BenchmarkBAPCaller_Discover_Percentiles` | p50 / p95 / p99 latency via `b.ReportMetric` | +| `BenchmarkBAPCaller_CacheWarm` | Latency when the Redis key cache is already populated | +| `BenchmarkBAPCaller_CacheCold` | Latency on a cold cache — full key-derivation round-trip | +| `BenchmarkBAPCaller_RPS` | Requests-per-second under parallel load (`req/s` custom metric) | + +--- + +## How It Works + +### Startup (`TestMain`) + +Before any benchmark runs, `TestMain` in `e2e/setup_test.go`: + +1. **Compiles all required plugins** to a temporary directory using `go build -buildmode=plugin`. The first run takes 60–90 s (cold Go build cache); subsequent runs are near-instant. +2. **Starts miniredis** — an in-process Redis server used by the `cache` plugin (no external Redis needed). +3. **Starts mock servers** — an instant-ACK BPP and a registry mock that returns the dev signing public key. +4. **Starts the adapter** — wires all plugins programmatically (no YAML parsing) and wraps it in an `httptest.Server`. + +### Per-iteration (`buildSignedRequest`) + +Each benchmark iteration: +1. Loads the JSON fixture for the requested Beckn action (`testdata/_request.json`). +2. Substitutes sentinel values (`BENCH_TIMESTAMP`, `BENCH_MESSAGE_ID`, `BENCH_TRANSACTION_ID`) with fresh values, ensuring unique message IDs per iteration. +3. Signs the body using the Beckn Ed25519/BLAKE-512 spec (same algorithm as the production `signer` plugin). +4. Sends the signed `POST` to the adapter and validates a `200 OK` response. + +### Validation test (`TestSignBecknPayload`) + +A plain `Test*` function runs before the benchmarks and sends one signed request end-to-end. If the signing helper is mis-implemented, this fails fast before any benchmark time is wasted. + +--- + +## Directory Layout + +``` +benchmarks/ +├── README.md ← you are here +├── run_benchmarks.sh ← one-shot runner script +├── e2e/ +│ ├── bench_test.go ← benchmark functions (T8) +│ ├── setup_test.go ← TestMain, startAdapter, signing helper (T3/T4/T7) +│ ├── mocks_test.go ← mock BPP and registry servers (T5) +│ ├── keys_test.go ← dev key pair constants (T6a) +│ └── testdata/ +│ ├── routing-BAPCaller.yaml ← routing config (BENCH_BPP_URL placeholder) +│ ├── discover_request.json ← Beckn search payload fixture +│ ├── select_request.json +│ ├── init_request.json +│ └── confirm_request.json +├── tools/ +│ └── parse_results.go ← CSV exporter for latency + throughput data (T10) +└── results/ + └── BENCHMARK_REPORT.md ← report template (populate after a run) +``` + +--- + +## Running Individual Benchmarks + +```bash +# Single benchmark, 10 s +go test ./benchmarks/e2e/... \ + -bench=BenchmarkBAPCaller_Discover \ + -benchtime=10s -benchmem -timeout=30m + +# All actions in one shot +go test ./benchmarks/e2e/... \ + -bench=BenchmarkBAPCaller_AllActions \ + -benchtime=5s -benchmem -timeout=30m + +# Concurrency sweep at 1, 4, and 16 goroutines +go test ./benchmarks/e2e/... \ + -bench=BenchmarkBAPCaller_Discover_Parallel \ + -benchtime=30s -cpu=1,4,16 -timeout=30m + +# Race detector check (no data races) +go test ./benchmarks/e2e/... \ + -bench=BenchmarkBAPCaller_Discover_Parallel \ + -benchtime=5s -race -timeout=30m + +# Percentile metrics (p50/p95/p99 in µs) +go test ./benchmarks/e2e/... \ + -bench=BenchmarkBAPCaller_Discover_Percentiles \ + -benchtime=10s -benchmem -timeout=30m +``` + +## Comparing Two Runs with benchstat + +```bash +go test ./benchmarks/e2e/... -bench=. -benchtime=10s -count=6 > before.txt +# ... make your change ... +go test ./benchmarks/e2e/... -bench=. -benchtime=10s -count=6 > after.txt +benchstat before.txt after.txt +``` + +--- + +## Dependencies + +| Package | Purpose | +|---------|---------| +| `github.com/alicebob/miniredis/v2` | In-process Redis for the `cache` plugin | +| `golang.org/x/perf/cmd/benchstat` | Statistical benchmark comparison (CLI tool) | + +Both are declared in `go.mod`. Run `go mod tidy` once to fetch their checksums. diff --git a/benchmarks/e2e/bench_test.go b/benchmarks/e2e/bench_test.go new file mode 100644 index 0000000..2775802 --- /dev/null +++ b/benchmarks/e2e/bench_test.go @@ -0,0 +1,186 @@ +package e2e_bench_test + +import ( + "fmt" + "net/http" + "sort" + "testing" + "time" +) + +// ── BenchmarkBAPCaller_Discover ─────────────────────────────────────────────── +// Baseline single-goroutine throughput and latency for the discover endpoint. +// Exercises the full bapTxnCaller pipeline: addRoute → sign → validateSchema. +func BenchmarkBAPCaller_Discover(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + req := buildSignedRequest(b, "discover") + if err := sendRequest(req); err != nil { + b.Errorf("iteration %d: %v", i, err) + } + } +} + +// ── BenchmarkBAPCaller_Discover_Parallel ───────────────────────────────────── +// Measures throughput under concurrent load. Run with -cpu=1,2,4,8,16 to +// produce a concurrency sweep. Each goroutine runs its own request loop. +func BenchmarkBAPCaller_Discover_Parallel(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + req := buildSignedRequest(b, "discover") + if err := sendRequest(req); err != nil { + b.Errorf("parallel: %v", err) + } + } + }) +} + +// ── BenchmarkBAPCaller_AllActions ──────────────────────────────────────────── +// Measures per-action latency for discover, select, init, and confirm in a +// single benchmark run. Each sub-benchmark is independent. +func BenchmarkBAPCaller_AllActions(b *testing.B) { + actions := []string{"discover", "select", "init", "confirm"} + + for _, action := range actions { + action := action // capture for sub-benchmark closure + b.Run(action, func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + req := buildSignedRequest(b, action) + if err := sendRequest(req); err != nil { + b.Errorf("action %s iteration %d: %v", action, i, err) + } + } + }) + } +} + +// ── BenchmarkBAPCaller_Discover_Percentiles ─────────────────────────────────── +// Collects individual request durations and reports p50, p95, and p99 latency +// in microseconds via b.ReportMetric. The percentile data is only meaningful +// when -benchtime is at least 5s (default used in run_benchmarks.sh). +func BenchmarkBAPCaller_Discover_Percentiles(b *testing.B) { + durations := make([]time.Duration, 0, b.N) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + req := buildSignedRequest(b, "discover") + start := time.Now() + if err := sendRequest(req); err != nil { + b.Errorf("iteration %d: %v", i, err) + continue + } + durations = append(durations, time.Since(start)) + } + + // Compute and report percentiles. + if len(durations) == 0 { + return + } + sort.Slice(durations, func(i, j int) bool { return durations[i] < durations[j] }) + + p50 := durations[len(durations)*50/100] + p95 := durations[len(durations)*95/100] + p99 := durations[len(durations)*99/100] + + b.ReportMetric(float64(p50.Microseconds()), "p50_µs") + b.ReportMetric(float64(p95.Microseconds()), "p95_µs") + b.ReportMetric(float64(p99.Microseconds()), "p99_µs") +} + +// ── BenchmarkBAPCaller_CacheWarm / CacheCold ───────────────────────────────── +// Compares latency when the Redis cache holds a pre-warmed key set (CacheWarm) +// vs. when each iteration has a fresh message_id that the cache has never seen +// (CacheCold). The delta reveals the key-lookup overhead on a cold path. + +// BenchmarkBAPCaller_CacheWarm sends a fixed body (constant message_id) so the +// simplekeymanager's Redis cache is hit on every iteration after the first. +func BenchmarkBAPCaller_CacheWarm(b *testing.B) { + body := warmFixtureBody(b, "discover") + + // Warm-up: send once to populate the cache before the timer starts. + warmReq := buildSignedRequestFixed(b, "discover", body) + if err := sendRequest(warmReq); err != nil { + b.Fatalf("cache warm-up request failed: %v", err) + } + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + req := buildSignedRequestFixed(b, "discover", body) + if err := sendRequest(req); err != nil { + b.Errorf("CacheWarm iteration %d: %v", i, err) + } + } +} + +// BenchmarkBAPCaller_CacheCold uses a fresh message_id per iteration, so every +// request experiences a cache miss and a full key-derivation round-trip. +func BenchmarkBAPCaller_CacheCold(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + req := buildSignedRequest(b, "discover") // fresh IDs each time + if err := sendRequest(req); err != nil { + b.Errorf("CacheCold iteration %d: %v", i, err) + } + } +} + +// ── BenchmarkBAPCaller_RPS ──────────────────────────────────────────────────── +// Reports requests-per-second as a custom metric alongside the default ns/op. +// Run with -benchtime=30s for a stable RPS reading. +func BenchmarkBAPCaller_RPS(b *testing.B) { + b.ReportAllocs() + + var count int64 + start := time.Now() + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + var local int64 + for pb.Next() { + req := buildSignedRequest(b, "discover") + if err := sendRequest(req); err == nil { + local++ + } + } + // Accumulate without atomic for simplicity — final value only read after + // RunParallel returns and all goroutines have exited. + count += local + }) + + elapsed := time.Since(start).Seconds() + if elapsed > 0 { + rps := float64(count) / elapsed + b.ReportMetric(rps, "req/s") + fmt.Printf(" RPS: %.0f over %.1fs\n", rps, elapsed) + } +} + +// ── helper: one-shot HTTP client ───────────────────────────────────────────── + +// benchHTTPClient is a shared client for all benchmark goroutines. +// MaxConnsPerHost caps the total active connections to localhost so we don't +// exhaust the OS ephemeral port range. MaxIdleConnsPerHost keeps that many +// connections warm in the pool so parallel goroutines reuse them rather than +// opening fresh TCP connections on every request. +var benchHTTPClient = &http.Client{ + Transport: &http.Transport{ + MaxIdleConns: 200, + MaxIdleConnsPerHost: 200, + MaxConnsPerHost: 200, + IdleConnTimeout: 90 * time.Second, + DisableCompression: true, // no benefit compressing localhost traffic + }, +} diff --git a/benchmarks/e2e/keys_test.go b/benchmarks/e2e/keys_test.go new file mode 100644 index 0000000..ac70482 --- /dev/null +++ b/benchmarks/e2e/keys_test.go @@ -0,0 +1,13 @@ +package e2e_bench_test + +// Development key pair from config/local-retail-bap.yaml. +// Used across the retail devkit for non-production testing. +// DO NOT use in any production or staging environment. +const ( + benchSubscriberID = "sandbox.food-finder.com" + benchKeyID = "76EU7VwahYv4XztXJzji9ssiSV74eWXWBcCKGn7jAdm5VGLCdYAJ8j" + benchPrivKey = "rrNtVgyASCGlo+ebsJaA37D5CZYZVfT0JA5/vlkTeV0=" + benchPubKey = "oFIk7KqCqvqRYkLMjQqiaKM5oOozkYT64bfLuc8p/SU=" + benchEncrPrivKey = "rrNtVgyASCGlo+ebsJaA37D5CZYZVfT0JA5/vlkTeV0=" + benchEncrPubKey = "oFIk7KqCqvqRYkLMjQqiaKM5oOozkYT64bfLuc8p/SU=" +) diff --git a/benchmarks/e2e/mocks_test.go b/benchmarks/e2e/mocks_test.go new file mode 100644 index 0000000..54d3543 --- /dev/null +++ b/benchmarks/e2e/mocks_test.go @@ -0,0 +1,63 @@ +package e2e_bench_test + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "time" +) + +// startMockBPP starts an httptest server that accepts any POST request and +// immediately returns a valid Beckn ACK. This replaces the real BPP backend, +// isolating benchmark results to adapter-internal latency only. +func startMockBPP() *httptest.Server { + ackBody := `{"message":{"ack":{"status":"ACK"}}}` + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, ackBody) + })) +} + +// subscriberRecord mirrors the registry API response shape for a single subscriber. +type subscriberRecord struct { + SubscriberID string `json:"subscriber_id"` + UniqueKeyID string `json:"unique_key_id"` + SigningPublicKey string `json:"signing_public_key"` + ValidFrom string `json:"valid_from"` + ValidUntil string `json:"valid_until"` + Status string `json:"status"` +} + +// startMockRegistry starts an httptest server that returns a subscriber record +// matching the benchmark test keys. The signvalidator plugin uses this to +// resolve the public key for signature verification on incoming requests. +func startMockRegistry() *httptest.Server { + record := subscriberRecord{ + SubscriberID: benchSubscriberID, + UniqueKeyID: benchKeyID, + SigningPublicKey: benchPubKey, + ValidFrom: time.Now().AddDate(-1, 0, 0).Format(time.RFC3339), + ValidUntil: time.Now().AddDate(10, 0, 0).Format(time.RFC3339), + Status: "SUBSCRIBED", + } + body, _ := json.Marshal([]subscriberRecord{record}) + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Support both GET (lookup) and POST (lookup with body) registry calls. + // Respond with the subscriber record regardless of subscriber_id query param. + subscriberID := r.URL.Query().Get("subscriber_id") + if subscriberID == "" { + // Try extracting from path for dedi-registry style calls. + parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/") + if len(parts) > 0 { + subscriberID = parts[len(parts)-1] + } + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(body) + })) +} diff --git a/benchmarks/e2e/setup_test.go b/benchmarks/e2e/setup_test.go new file mode 100644 index 0000000..702c5da --- /dev/null +++ b/benchmarks/e2e/setup_test.go @@ -0,0 +1,466 @@ +package e2e_bench_test + +import ( + "bytes" + "context" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + "github.com/beckn-one/beckn-onix/core/module" + "github.com/beckn-one/beckn-onix/core/module/handler" + "github.com/beckn-one/beckn-onix/pkg/model" + "github.com/beckn-one/beckn-onix/pkg/plugin" + "github.com/google/uuid" + "github.com/rs/zerolog" + "golang.org/x/crypto/blake2b" +) + +// Package-level references shared across all benchmarks. +var ( + adapterServer *httptest.Server + miniRedis *miniredis.Miniredis + mockBPP *httptest.Server + mockRegistry *httptest.Server + pluginDir string + moduleRoot string // set in TestMain; used by buildBAPCallerConfig for local file paths +) + +// Plugins to compile for the benchmark. Each entry is (pluginID, source path relative to module root). +var pluginsToBuild = []struct { + id string + src string +}{ + {"router", "pkg/plugin/implementation/router/cmd/plugin.go"}, + {"signer", "pkg/plugin/implementation/signer/cmd/plugin.go"}, + {"signvalidator", "pkg/plugin/implementation/signvalidator/cmd/plugin.go"}, + {"simplekeymanager", "pkg/plugin/implementation/simplekeymanager/cmd/plugin.go"}, + {"cache", "pkg/plugin/implementation/cache/cmd/plugin.go"}, + {"schemav2validator", "pkg/plugin/implementation/schemav2validator/cmd/plugin.go"}, + {"otelsetup", "pkg/plugin/implementation/otelsetup/cmd/plugin.go"}, + // registry is required by stdHandler to wire KeyManager, even on the caller + // path where sign-validation never runs. + {"registry", "pkg/plugin/implementation/registry/cmd/plugin.go"}, +} + +// TestMain is the entry point for the benchmark package. It: +// 1. Compiles all required .so plugins into a temp directory +// 2. Starts miniredis (in-process Redis) +// 3. Starts mock BPP and registry HTTP servers +// 4. Starts the adapter as an httptest.Server +// 5. Runs all benchmarks +// 6. Tears everything down in reverse order +func TestMain(m *testing.M) { + ctx := context.Background() + + // ── Step 1: Compile plugins ─────────────────────────────────────────────── + var err error + pluginDir, err = os.MkdirTemp("", "beckn-bench-plugins-*") + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: failed to create plugin temp dir: %v\n", err) + os.Exit(1) + } + defer os.RemoveAll(pluginDir) + + moduleRoot, err = findModuleRoot() + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: failed to locate module root: %v\n", err) + os.Exit(1) + } + + fmt.Printf("=== Building plugins (first run may take 60-90s) ===\n") + for _, p := range pluginsToBuild { + outPath := filepath.Join(pluginDir, p.id+".so") + srcPath := filepath.Join(moduleRoot, p.src) + fmt.Printf(" compiling %s.so ...\n", p.id) + cmd := exec.Command("go", "build", "-buildmode=plugin", "-o", outPath, srcPath) + cmd.Dir = moduleRoot + if out, buildErr := cmd.CombinedOutput(); buildErr != nil { + fmt.Fprintf(os.Stderr, "ERROR: failed to build plugin %s:\n%s\n", p.id, string(out)) + os.Exit(1) + } + } + fmt.Printf("=== All plugins compiled successfully ===\n\n") + + // ── Step 2: Start miniredis ─────────────────────────────────────────────── + miniRedis, err = miniredis.Run() + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: failed to start miniredis: %v\n", err) + os.Exit(1) + } + defer miniRedis.Close() + + // ── Step 3: Start mock servers ──────────────────────────────────────────── + mockBPP = startMockBPP() + defer mockBPP.Close() + + mockRegistry = startMockRegistry() + defer mockRegistry.Close() + + // ── Step 4: Start adapter ───────────────────────────────────────────────── + adapterServer, err = startAdapter(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: failed to start adapter: %v\n", err) + os.Exit(1) + } + defer adapterServer.Close() + + // ── Step 5: Run benchmarks ──────────────────────────────────────────────── + // Silence the adapter's zerolog output for the duration of the benchmark + // run. Without this, every HTTP request the adapter processes emits a JSON + // log line to stdout, which interleaves with Go's benchmark result lines + // (BenchmarkFoo-N\t\t\t) and makes benchstat unparseable. + // Setup logging above still ran normally; zerolog.Disabled is set only here, + // just before m.Run(), so errors during startup remain visible. + zerolog.SetGlobalLevel(zerolog.Disabled) + os.Exit(m.Run()) +} + +// findModuleRoot walks up from the current directory to find the go.mod root. +func findModuleRoot() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + for { + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + return dir, nil + } + parent := filepath.Dir(dir) + if parent == dir { + return "", fmt.Errorf("go.mod not found from %s", dir) + } + dir = parent + } +} + +// writeRoutingConfig reads the benchmark routing config template, replaces the +// BENCH_BPP_URL placeholder with the live mock BPP server URL, and writes the +// result to a temp file. Returns the path to the temp file. +func writeRoutingConfig(bppURL string) (string, error) { + templatePath := filepath.Join("testdata", "routing-BAPCaller.yaml") + data, err := os.ReadFile(templatePath) + if err != nil { + return "", fmt.Errorf("reading routing config template: %w", err) + } + content := strings.ReplaceAll(string(data), "BENCH_BPP_URL", bppURL) + f, err := os.CreateTemp("", "bench-routing-*.yaml") + if err != nil { + return "", fmt.Errorf("creating temp routing config: %w", err) + } + if _, err := f.WriteString(content); err != nil { + f.Close() + return "", fmt.Errorf("writing routing config: %w", err) + } + f.Close() + return f.Name(), nil +} + +// startAdapter constructs a fully wired adapter using the compiled plugins and +// returns it as an *httptest.Server. All external dependencies are replaced with +// local mock servers: Redis → miniredis, BPP → mockBPP, registry → mockRegistry. +func startAdapter(ctx context.Context) (*httptest.Server, error) { + routingConfigPath, err := writeRoutingConfig(mockBPP.URL) + if err != nil { + return nil, fmt.Errorf("writing routing config: %w", err) + } + + // Plugin manager: load all compiled .so files from pluginDir. + mgr, closer, err := plugin.NewManager(ctx, &plugin.ManagerConfig{ + Root: pluginDir, + }) + if err != nil { + return nil, fmt.Errorf("creating plugin manager: %w", err) + } + _ = closer // closer is called when the server shuts down; deferred in TestMain via server.Close + + // Build module configurations. + mCfgs := []module.Config{ + buildBAPCallerConfig(routingConfigPath, mockRegistry.URL), + } + + mux := http.NewServeMux() + if err := module.Register(ctx, mCfgs, mux, mgr); err != nil { + return nil, fmt.Errorf("registering modules: %w", err) + } + + srv := httptest.NewServer(mux) + return srv, nil +} + +// buildBAPCallerConfig returns the module.Config for the bapTxnCaller handler, +// mirroring config/local-retail-bap.yaml but pointing at benchmark mock services. +// registryURL must point at the mock registry so simplekeymanager can satisfy the +// Registry requirement imposed by stdHandler — even though the caller path never +// performs signature validation, the handler wiring requires it to be present. +func buildBAPCallerConfig(routingConfigPath, registryURL string) module.Config { + return module.Config{ + Name: "bapTxnCaller", + Path: "/bap/caller/", + Handler: handler.Config{ + Type: handler.HandlerTypeStd, + Role: model.RoleBAP, + SubscriberID: benchSubscriberID, + HttpClientConfig: handler.HttpClientConfig{ + MaxIdleConns: 1000, + MaxIdleConnsPerHost: 200, + IdleConnTimeout: 300 * time.Second, + ResponseHeaderTimeout: 5 * time.Second, + }, + Plugins: handler.PluginCfg{ + // Registry is required by stdHandler before it will wire KeyManager, + // even on the caller path where sign-validation never runs. We point + // it at the mock registry (retry_max=0 so failures are immediate). + Registry: &plugin.Config{ + ID: "registry", + Config: map[string]string{ + "url": registryURL, + "retry_max": "0", + }, + }, + KeyManager: &plugin.Config{ + ID: "simplekeymanager", + Config: map[string]string{ + "networkParticipant": benchSubscriberID, + "keyId": benchKeyID, + "signingPrivateKey": benchPrivKey, + "signingPublicKey": benchPubKey, + "encrPrivateKey": benchEncrPrivKey, + "encrPublicKey": benchEncrPubKey, + }, + }, + SchemaValidator: &plugin.Config{ + ID: "schemav2validator", + Config: map[string]string{ + "type": "file", + "location": filepath.Join(moduleRoot, "benchmarks/e2e/testdata/beckn.yaml"), + "cacheTTL": "3600", + }, + }, + Cache: &plugin.Config{ + ID: "cache", + Config: map[string]string{ + "addr": miniRedis.Addr(), + }, + }, + Router: &plugin.Config{ + ID: "router", + Config: map[string]string{ + "routingConfig": routingConfigPath, + }, + }, + Signer: &plugin.Config{ + ID: "signer", + }, + }, + Steps: []string{"addRoute", "sign", "validateSchema"}, + }, + } +} + +// ── T7: Request builder and Beckn signing helper ────────────────────────────── + +// becknPayloadTemplate holds the raw JSON for a fixture file with sentinels. +var fixtureCache = map[string][]byte{} + +// loadFixture reads a fixture file from testdata/ and caches it. +func loadFixture(action string) ([]byte, error) { + if data, ok := fixtureCache[action]; ok { + return data, nil + } + path := filepath.Join("testdata", action+"_request.json") + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("loading fixture %s: %w", action, err) + } + fixtureCache[action] = data + return data, nil +} + +// buildSignedRequest reads the fixture for the given action, substitutes +// BENCH_TIMESTAMP / BENCH_MESSAGE_ID / BENCH_TRANSACTION_ID with fresh values, +// signs the body using the Beckn Ed25519 spec, and returns a ready-to-send +// *http.Request targeting the adapter's /bap/caller/ path. +func buildSignedRequest(tb testing.TB, action string) *http.Request { + tb.Helper() + + fixture, err := loadFixture(action) + if err != nil { + tb.Fatalf("buildSignedRequest: %v", err) + } + + // Substitute sentinels with fresh values for this iteration. + now := time.Now().UTC().Format(time.RFC3339) + msgID := uuid.New().String() + txnID := uuid.New().String() + + body := bytes.ReplaceAll(fixture, []byte("BENCH_TIMESTAMP"), []byte(now)) + body = bytes.ReplaceAll(body, []byte("BENCH_MESSAGE_ID"), []byte(msgID)) + body = bytes.ReplaceAll(body, []byte("BENCH_TRANSACTION_ID"), []byte(txnID)) + + // Sign the body per the Beckn Ed25519 spec. + authHeader, err := signBecknPayload(body) + if err != nil { + tb.Fatalf("buildSignedRequest: signing failed: %v", err) + } + + url := adapterServer.URL + "/bap/caller/" + action + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + tb.Fatalf("buildSignedRequest: http.NewRequest: %v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set(model.AuthHeaderSubscriber, authHeader) + + return req +} + +// buildSignedRequestFixed builds a signed request with a fixed body (same +// message_id every call) — used for cache-warm benchmarks. +func buildSignedRequestFixed(tb testing.TB, action string, body []byte) *http.Request { + tb.Helper() + + authHeader, err := signBecknPayload(body) + if err != nil { + tb.Fatalf("buildSignedRequestFixed: signing failed: %v", err) + } + + url := adapterServer.URL + "/bap/caller/" + action + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + tb.Fatalf("buildSignedRequestFixed: http.NewRequest: %v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set(model.AuthHeaderSubscriber, authHeader) + return req +} + +// signBecknPayload signs a request body using the Beckn Ed25519 signing spec +// and returns a formatted Authorization header value. +// +// Beckn signing spec: +// 1. Digest: "BLAKE-512=" + base64(blake2b-512(body)) +// 2. Signing string: "(created): \n(expires): \ndigest: " +// 3. Signature: base64(ed25519.Sign(privKey, signingString)) +// 4. Header: Signature keyId="||ed25519",algorithm="ed25519", +// created="",expires="",headers="(created) (expires) digest", +// signature="" +// +// Reference: pkg/plugin/implementation/signer/signer.go +func signBecknPayload(body []byte) (string, error) { + createdAt := time.Now().Unix() + expiresAt := time.Now().Add(5 * time.Minute).Unix() + + // Step 1: BLAKE-512 digest. + hasher, _ := blake2b.New512(nil) + hasher.Write(body) + digest := "BLAKE-512=" + base64.StdEncoding.EncodeToString(hasher.Sum(nil)) + + // Step 2: Signing string. + signingString := fmt.Sprintf("(created): %d\n(expires): %d\ndigest: %s", createdAt, expiresAt, digest) + + // Step 3: Ed25519 signature. + privKeyBytes, err := base64.StdEncoding.DecodeString(benchPrivKey) + if err != nil { + return "", fmt.Errorf("decoding private key: %w", err) + } + privKey := ed25519.NewKeyFromSeed(privKeyBytes) + sig := base64.StdEncoding.EncodeToString(ed25519.Sign(privKey, []byte(signingString))) + + // Step 4: Format Authorization header (matches generateAuthHeader in step.go). + header := fmt.Sprintf( + `Signature keyId="%s|%s|ed25519",algorithm="ed25519",created="%d",expires="%d",headers="(created) (expires) digest",signature="%s"`, + benchSubscriberID, benchKeyID, createdAt, expiresAt, sig, + ) + return header, nil +} + +// warmFixtureBody returns a fixed body for the given action with stable IDs — +// used to pre-warm the cache so cache-warm benchmarks hit the Redis fast path. +func warmFixtureBody(tb testing.TB, action string) []byte { + tb.Helper() + fixture, err := loadFixture(action) + if err != nil { + tb.Fatalf("warmFixtureBody: %v", err) + } + body := bytes.ReplaceAll(fixture, []byte("BENCH_TIMESTAMP"), []byte("2025-01-01T00:00:00Z")) + body = bytes.ReplaceAll(body, []byte("BENCH_MESSAGE_ID"), []byte("00000000-warm-0000-0000-000000000000")) + body = bytes.ReplaceAll(body, []byte("BENCH_TRANSACTION_ID"), []byte("00000000-warm-txn-0000-000000000000")) + return body +} + +// sendRequest executes an HTTP request using the shared bench client and +// discards the response body. Returns a non-nil error for non-2xx responses. +func sendRequest(req *http.Request) error { + resp, err := benchHTTPClient.Do(req) + if err != nil { + return fmt.Errorf("http do: %w", err) + } + defer resp.Body.Close() + // Drain the body so the connection is returned to the pool for reuse. + // Without this, Go discards the connection after each request, causing + // port exhaustion under parallel load ("can't assign requested address"). + _, _ = io.Copy(io.Discard, resp.Body) + // We accept any 2xx response (ACK or forwarded BPP response). + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("unexpected status: %d", resp.StatusCode) + } + return nil +} + +// ── TestSignBecknPayload: validation test before running benchmarks ─────────── +// Sends a signed discover request to the live adapter and asserts a 200 response, +// confirming the signing helper produces headers accepted by the adapter pipeline. +func TestSignBecknPayload(t *testing.T) { + if adapterServer == nil { + t.Skip("adapterServer not initialised (run via TestMain)") + } + fixture, err := loadFixture("discover") + if err != nil { + t.Fatalf("loading fixture: %v", err) + } + + // Substitute sentinels. + now := time.Now().UTC().Format(time.RFC3339) + body := bytes.ReplaceAll(fixture, []byte("BENCH_TIMESTAMP"), []byte(now)) + body = bytes.ReplaceAll(body, []byte("BENCH_MESSAGE_ID"), []byte(uuid.New().String())) + body = bytes.ReplaceAll(body, []byte("BENCH_TRANSACTION_ID"), []byte(uuid.New().String())) + + authHeader, err := signBecknPayload(body) + if err != nil { + t.Fatalf("signBecknPayload: %v", err) + } + + url := adapterServer.URL + "/bap/caller/discover" + req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + t.Fatalf("http.NewRequest: %v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set(model.AuthHeaderSubscriber, authHeader) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("sending request: %v", err) + } + defer resp.Body.Close() + + var result map[string]interface{} + json.NewDecoder(resp.Body).Decode(&result) + t.Logf("Response status: %d, body: %v", resp.StatusCode, result) + + if resp.StatusCode != http.StatusOK { + t.Errorf("expected 200 OK, got %d", resp.StatusCode) + } +} diff --git a/benchmarks/e2e/testdata/beckn.yaml b/benchmarks/e2e/testdata/beckn.yaml new file mode 100644 index 0000000..dc7f473 --- /dev/null +++ b/benchmarks/e2e/testdata/beckn.yaml @@ -0,0 +1,3380 @@ +openapi: 3.1.1 +info: + title: Beckn Protocol API + description: | + The Beckn Protocol API Specification v2.0 — a concrete, developer-friendly OpenAPI 3.1.1 + specification with one named path per Beckn protocol action. + + ## Design Philosophy + - **Minimalism**: Transport-layer schemas are inline. All data-model schemas + (Catalog, Contract, Intent, etc.) are resolved from #/components/schemas. + - **Pragmatism**: Every transaction endpoint carries both `order` (legacy, backward-compatible) + and `contract` (new, generalized) in its message envelope via `oneOf`. + - **Backward compatibility**: Implementations conformant with previous versions interoperate + seamlessly with new implementations using the `contract` model. + + ## Typical Transaction Lifecycle + ``` + discover → on_discover + select → on_select → init → on_init → confirm → on_confirm + → [status / on_status]* → [update / on_update]* → [cancel / on_cancel]? + → [track / on_track]* → rate / on_rate → support / on_support + ``` + + ## (NEW) `context.try` — Sandbox Mode + The `try` flag on the `Context` object (defined in `#/components/schemas/Context`) + indicates a sandbox mode being enabled on the network participant: + - `context.try: true` — Request to test an endpoint without liabilities + **before** committing. The receiver responds with expected consequences without changing state. + - `context.try: false` (default) — Commit the action. + This pattern is applicable to `update`, `cancel`, `rate`, and `support` actions. + + ## Authentication + All requests MUST carry a valid Beckn HTTP Signature in the `Authorization` header. + The `Ack` response body MUST carry a `CounterSignature` proving receipt. + See `docs/10_Signing_Beckn_APIs_in_HTTP.md` for the signing specification. + + ## Compatibility Scope + This specification is designed for interoperability across ALL value-exchange use cases: + retail, mobility, logistics, energy, healthcare, skilling, financial services, + carbon trading, hiring, data licensing, and any other domain built on Beckn. + version: 2.0.0 + contact: + name: Beckn Protocol + url: https://beckn.io + license: + name: CC-BY-NC-SA 4.0 International + url: https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en + +tags: + - name: Discovery + description: Discover catalogs across a Beckn network + - name: Transaction + description: Select, negotiate, confirm and manage contracts/orders + - name: Fulfillment + description: Track, update and cancel active contracts/orders + - name: Post-Fulfillment + description: Rate and support completed contracts/orders + - name: Catalog Publishing + description: Publish catalogs to a Catalog Discovery Service (CDS) + +# All requests MUST carry a valid Beckn HTTP Signature in the Authorization header. +# The securityScheme is informational; actual enforcement is done at the network layer. +security: [] + +paths: + + # ───────────────────────────────────────────────────────────────────── + # DISCOVERY + # ───────────────────────────────────────────────────────────────────── + + /discover: + post: + summary: Discover catalogs on a network + description: | + Search and discover catalogs on an open network. Supports text search (textSearch), + JSONPath-based filtering (RFC 9535), spatial (geo) constraints, and media search. + Returns an **ACK** confirming receipt; actual catalogs arrive via `/beckn/on_discover`. + Can be implemented by Catalog Discovery Services (CDS) and BPPs. + operationId: discover + tags: [Discovery] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' # TODO: fetch this from schema.beckn.io/Context/v2.0 + - type: object + properties: + action: + type: string + const: discover + message: + $ref: '#/components/schemas/DiscoverAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '409': + $ref: '#/components/responses/AckNoCallback' + '500': + $ref: '#/components/responses/ServerError' + + /on_discover: + post: + summary: Callback — return catalogs to the BAP + description: | + Callback from a BPP or CDS containing catalogs matching the discovery request. + Returns an **ACK** confirming receipt of the catalog payload. + operationId: onDiscover + tags: [Discovery] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: on_discover + message: + $ref: '#/components/schemas/OnDiscoverAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '500': + $ref: '#/components/responses/ServerError' + + # ───────────────────────────────────────────────────────────────────── + # TRANSACTION — ORDERING + # ───────────────────────────────────────────────────────────────────── + + /select: + post: + summary: BAP selects items/resources and requests a quote + description: | + The BAP selects catalog items, item quantities, associated offers, and requests + the BPP to provide a priced quote. No PII is shared at this stage. + Supports both the legacy `order` and the new `contract` model. + operationId: select + tags: [Transaction] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: select + message: + $ref: '#/components/schemas/SelectAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '409': + $ref: '#/components/responses/AckNoCallback' + '500': + $ref: '#/components/responses/ServerError' + + /on_select: + post: + summary: BPP returns a priced quote + description: | + The BPP calculates the total cost, evaluates offers, and returns a priced quote. + Supports both the legacy `order` model and the new `contract` model. + operationId: onSelect + tags: [Transaction] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: on_select + message: + $ref: '#/components/schemas/OnSelectAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '500': + $ref: '#/components/responses/ServerError' + + /init: + post: + summary: BAP initiates final order — submits billing, fulfillment and payment intent + description: | + BAP provides billing, fulfillment, and payment information for the BPP to generate + final order terms. The BPP responds via `on_init` with the final draft including + payment terms. Supports both `order` (legacy) and `contract` (new) models. + operationId: init + tags: [Transaction] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: init + message: + $ref: '#/components/schemas/InitAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '409': + $ref: '#/components/responses/AckNoCallback' + '500': + $ref: '#/components/responses/ServerError' + + /on_init: + post: + summary: BPP returns final draft order/contract with payment terms + description: | + BPP returns the final draft order or contract with personalized payment terms. + The BPP MUST NOT request payment before this stage. + operationId: onInit + tags: [Transaction] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: on_init + message: + $ref: '#/components/schemas/OnInitAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '500': + $ref: '#/components/responses/ServerError' + + /confirm: + post: + summary: BAP confirms the order/contract + description: | + BAP accepts all terms and provides payment proof/reference, then requests the BPP + to confirm the order or contract into a binding commitment. + operationId: confirm + tags: [Transaction] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: confirm + message: + $ref: '#/components/schemas/ConfirmAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '409': + $ref: '#/components/responses/AckNoCallback' + '500': + $ref: '#/components/responses/ServerError' + + /on_confirm: + post: + summary: BPP returns the confirmed order/contract + description: | + BPP validates all terms, payment, and returns a confirmed order or contract with + its assigned ID and the initial fulfillment/performance state. + operationId: onConfirm + tags: [Transaction] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: on_confirm + message: + $ref: '#/components/schemas/OnConfirmAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '500': + $ref: '#/components/responses/ServerError' + + # ───────────────────────────────────────────────────────────────────── + # FULFILLMENT + # ───────────────────────────────────────────────────────────────────── + + /status: + post: + summary: BAP requests the current state of an order/contract + description: | + BAP requests the latest state of an active order or contract by providing its ID. + The BPP responds via `on_status`. BAPs SHOULD NOT long-poll with `status` — + use `on_status` push notifications for significant state changes. + operationId: status + tags: [Fulfillment] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: status + message: + $ref: '#/components/schemas/StatusAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '409': + $ref: '#/components/responses/AckNoCallback' + '500': + $ref: '#/components/responses/ServerError' + + /on_status: + post: + summary: BPP returns the current state of the order/contract + description: | + BPP returns the latest state of the order or contract including fulfillment/performance + updates. The BPP MAY call this endpoint proactively without the BAP requesting it, + but MUST have received at least one `status` request first to initiate the push stream. + This endpoint is for significant state changes worth a notification — NOT for + real-time location tracking (use `track`/`on_track` for that). + operationId: onStatus + tags: [Fulfillment] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: on_status + message: + $ref: '#/components/schemas/OnStatusAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '500': + $ref: '#/components/responses/ServerError' + + /track: + post: + summary: BAP requests a real-time tracking handle for an active order/contract + description: | + BAP requests a tracking handle (URL, WebSocket endpoint, or webhook) for real-time + tracking of an active fulfillment or performance stage. This is for real-time + position/progress updates — NOT for order state change notifications (use `status`). + operationId: track + tags: [Fulfillment] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: track + message: + $ref: '#/components/schemas/TrackAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '409': + $ref: '#/components/responses/AckNoCallback' + '500': + $ref: '#/components/responses/ServerError' + + /on_track: + post: + summary: BPP returns real-time tracking handle and status + operationId: onTrack + tags: [Fulfillment] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: on_track + message: + $ref: '#/components/schemas/OnTrackAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '500': + $ref: '#/components/responses/ServerError' + + /update: + post: + summary: BAP requests a mutation to an active order/contract + description: | + BAP requests the BPP to update an active order or contract. Updates may involve + change of items, quantities, billing, fulfillment/performance, or payment. + + Uses the `context.try` two-phase pattern (defined in `Context` schema): + - `context.try: true` — BAP requests updated terms before committing (preview mode). + BPP responds with recalculated quote and terms via `on_update` without changing state. + - `context.try: false` (default) — BAP commits the update. + BPP responds with the confirmed updated order/contract. + operationId: update + tags: [Fulfillment] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + $ref: '#/components/schemas/Context' + message: + $ref: '#/components/schemas/UpdateAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '409': + $ref: '#/components/responses/AckNoCallback' + '500': + $ref: '#/components/responses/ServerError' + + /on_update: + post: + summary: BPP returns the updated order/contract + description: | + BPP returns the updated order or contract with recalculated terms. Can be called + proactively by the BPP without the BAP requesting it. + - If `context.try: true` — returns updated terms (preview; state not changed). + - If `context.try: false` — returns committed updated order/contract. + operationId: onUpdate + tags: [Fulfillment] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + $ref: '#/components/schemas/Context' + message: + $ref: '#/components/schemas/OnUpdateAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '500': + $ref: '#/components/responses/ServerError' + + /cancel: + post: + summary: BAP requests cancellation of an order/contract + description: | + BAP requests full or partial cancellation. Uses the `context.try` two-phase pattern: + - `context.try: true` — BAP requests cancellation terms (e.g., refund policy, penalty) + without committing to cancellation. BPP responds with applicable policy via `on_cancel`. + - `context.try: false` (default) — BAP commits the cancellation. + BPP confirms cancellation and returns cancelled order/contract. + operationId: cancel + tags: [Fulfillment] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + $ref: '#/components/schemas/Context' + message: + $ref: '#/components/schemas/CancelAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '409': + $ref: '#/components/responses/AckNoCallback' + '500': + $ref: '#/components/responses/ServerError' + + /on_cancel: + post: + summary: BPP returns cancellation terms or confirms cancellation + description: | + - If `context.try: true` — returns applicable cancellation policy, fees, refund timeline + (state not changed). + - If `context.try: false` — returns confirmed cancelled order/contract with refund/payment update. + operationId: onCancel + tags: [Fulfillment] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + $ref: '#/components/schemas/Context' + message: + $ref: '#/components/schemas/OnCancelAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '500': + $ref: '#/components/responses/ServerError' + + # ───────────────────────────────────────────────────────────────────── + # POST-FULFILLMENT + # ───────────────────────────────────────────────────────────────────── + + /rate: + post: + summary: BAP submits ratings for one or more entities + description: | + BAP submits ratings for any ratable entity (order, contract, fulfillment, + item, provider, agent, support interaction). Supports the `context.try` pattern: + - `context.try: true` — Preview: BPP returns the rating form/criteria without recording. + - `context.try: false` (default) — Submit: BPP records the rating and returns confirmation. + The BPP acknowledges via `on_rate` with an optional aggregate snapshot. + operationId: rate + tags: [Post-Fulfillment] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + $ref: '#/components/schemas/Context' + message: + $ref: '#/components/schemas/RateAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '409': + $ref: '#/components/responses/AckNoCallback' + '500': + $ref: '#/components/responses/ServerError' + + /on_rate: + post: + summary: BPP acknowledges rating and optionally returns aggregate + description: | + - If `context.try: true` — returns rating form/criteria (preview mode). + - If `context.try: false` — confirms rating was recorded; optionally returns aggregate. + operationId: onRate + tags: [Post-Fulfillment] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + $ref: '#/components/schemas/Context' + message: + $ref: '#/components/schemas/OnRateAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '500': + $ref: '#/components/responses/ServerError' + + /support: + post: + summary: BAP requests support information for an entity + description: | + BAP requests support channels and/or raises a support ticket for any entity. + Uses the `context.try` pattern: + - `context.try: true` — Preview: BPP returns available support channels without creating a ticket. + - `context.try: false` (default) — BAP submits a support request; BPP creates/links a ticket. + The `refType` field abstracts over domain-specific entity types (works for contracts, + resources, providers, agents — not just orders). + operationId: support + tags: [Post-Fulfillment] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + $ref: '#/components/schemas/Context' + message: + $ref: '#/components/schemas/SupportAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '409': + $ref: '#/components/responses/AckNoCallback' + '500': + $ref: '#/components/responses/ServerError' + + /on_support: + post: + summary: BPP returns support details + description: | + - If `context.try: true` — returns available support channels (preview mode). + - If `context.try: false` — returns support details with ticket(s). + operationId: onSupport + tags: [Post-Fulfillment] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + $ref: '#/components/schemas/Context' + message: + $ref: '#/components/schemas/OnSupportAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '500': + $ref: '#/components/responses/ServerError' + + # ───────────────────────────────────────────────────────────────────── + # CATALOG PUBLISHING SERVICE + # ───────────────────────────────────────────────────────────────────── + + /catalog/publish: + post: + summary: BPP publishes catalogs for indexing by a CDS + description: | + BPP submits one or more catalogs to be indexed by a Catalog Discovery Service (CDS). + Returns an **ACK** immediately if accepted for processing. The per-catalog processing + result arrives asynchronously via `/beckn/catalog/on_publish`. + operationId: catalogPublish + tags: [Catalog Publishing] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: catalog/publish + message: + $ref: '#/components/schemas/CatalogPublishAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '409': + $ref: '#/components/responses/AckNoCallback' + '500': + $ref: '#/components/responses/ServerError' + + /catalog/on_publish: + post: + summary: CDS returns per-catalog processing results to the BPP + operationId: catalogOnPublish + tags: [Catalog Publishing] + parameters: + - $ref: '#/components/parameters/AuthorizationHeader' + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [context, message] + properties: + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + type: string + const: catalog/on_publish + message: + $ref: '#/components/schemas/CatalogPublishAction' + responses: + '200': + $ref: '#/components/responses/Ack' + '400': + $ref: '#/components/responses/NackBadRequest' + '401': + $ref: '#/components/responses/NackUnauthorized' + '500': + $ref: '#/components/responses/ServerError' + +# ───────────────────────────────────────────────────────────────────── +# COMPONENTS +# ───────────────────────────────────────────────────────────────────── +components: + + # ─── Parameters ────────────────────────────────────────────────── + parameters: + AuthorizationHeader: + name: Authorization + in: header + required: true + description: | + Beckn HTTP Signature authentication header. + See `#/components/schemas/Signature` for the expected format. + schema: + $ref: '#/components/schemas/Signature' + + # ─── Message Schemas ────────────────────────────────────────────── + + schemas: + # ───────────────────────────────────────────────────────────────────── + # ACTION SCHEMAS — xAction and On-xAction message payloads + # ───────────────────────────────────────────────────────────────────── + CancelAction: + $id: https://schema.beckn.io/CancelAction/v2.0 + title: Cancel Action + description: | + Beckn /beckn/cancel message payload. Sent by a BAP to a BPP to request + cancellation of an active contract. Set context.try to true to first + retrieve cancellation terms and fees before committing. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + required: [order] + properties: + order: + type: object + - type: object + required: [contract] + properties: + contract: + $ref: '#/components/schemas/Contract' + + CatalogPublishAction: + $id: https://schema.beckn.io/CatalogPublishAction/v2.0 + description: Catalog publish request payload. + type: object + required: [catalogs] + properties: + catalogPublishAttributes: + type: object + description: Domain-specific extension attributes for this catalog publish action. + properties: + '@context': + type: array + items: + type: string + format: uri + uniqueItems: true + contains: + const: 'https://schema.beckn.io/CatalogPublishAction/v2.0/context.jsonld' + minItems: 1 + '@type': + type: array + items: + type: string + format: uri + uniqueItems: true + contains: + const: 'beckn:CatalogPublishAction' + minItems: 1 + catalogs: + type: array + minItems: 1 + description: One or more catalogs to be indexed by the CDS + items: + $ref: '#/components/schemas/Catalog' + + OnCatalogPublishAction: + $id: https://schema.beckn.io/CatalogPublishResponse/v2.0 + type: object + required: [context, message] + properties: + onCatalogPublishAttributes: + type: object + description: Domain-specific extension attributes for this on_catalog_publish action. + properties: + '@context': + type: array + items: + type: string + format: uri + uniqueItems: true + contains: + const: 'https://schema.beckn.io/OnCatalogPublishAction/v2.0/context.jsonld' + minItems: 1 + '@type': + type: array + items: + type: string + format: uri + uniqueItems: true + contains: + const: 'beckn:OnCatalogPublishAction' + minItems: 1 + context: + allOf: + - $ref: '#/components/schemas/Context' + - type: object + properties: + action: + const: 'on_catalog_publish' + + message: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/CatalogProcessingResult' + + ConfirmAction: + $id: https://schema.beckn.io/ConfirmAction/v2.0 + title: Confirm Action + description: | + Beckn /beckn/confirm message payload. Sent by a BAP to a BPP to confirm + a contract, finalising the transaction terms agreed during the + select-init negotiation cycle. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + required: [order] + properties: + order: + type: object + - type: object + required: [contract] + properties: + contract: + $ref: '#/components/schemas/Contract' + + DiscoverAction: + $id: https://schema.beckn.io/DiscoverAction/v2.0 + title: Discover Action (External) + description: | + Beckn /beckn/discover message payload as published at schema.beckn.io. + Supports two forms: intent properties directly at the top level (flat), + or all intent properties nested inside an `intent` container object. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - description: Flat form — Intent properties directly at top level (no intent container) + $ref: '#/components/schemas/Intent' + - description: Nested form — Intent properties inside an intent container object + type: object + required: [intent] + properties: + intent: + $ref: '#/components/schemas/Intent' + + InitAction: + $id: https://schema.beckn.io/InitAction/v2.0 + title: Init Action + description: | + Beckn /beckn/init message payload. Sent by a BAP to a BPP to initialise + a contract with consumer details (billing address, fulfillment preferences, etc.). + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + required: [order] + properties: + order: + type: object + - type: object + required: [contract] + properties: + contract: + $ref: '#/components/schemas/Contract' + + + OnCancelAction: + $id: https://schema.beckn.io/OnCancelAction/v2.0 + title: On Cancel Action + description: | + Beckn /beckn/on_cancel message payload. Sent by a BPP to a BAP in + response to a /beckn/cancel call, returning the contract with status + set to CANCELLED and any applicable cancellation outcome. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + required: [order] + properties: + order: + type: object + - type: object + required: [contract] + properties: + contract: + $ref: '#/components/schemas/Contract' + + OnConfirmAction: + $id: https://schema.beckn.io/OnConfirmAction/v2.0 + title: OnConfirmAction + description: | + Beckn /beckn/on_confirm message payload. Sent by a BPP to a BAP in + response to a /beckn/confirm call, returning the confirmed contract + with status set to CONFIRMED. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + required: [order] + properties: + order: + type: object + - type: object + required: [contract] + properties: + contract: + $ref: '#/components/schemas/Contract' + + OnDiscoverAction: + $id: https://schema.beckn.io/OnDiscoverAction/v2.0 + description: The on_discover response payload containing matching catalogs. + type: object + required: [catalogs] + properties: + catalogs: + type: array + description: | + Array of catalogs matching the discovery request. + Note: Each catalog's `items` array carries `oneOf [Item, Resource]` entries: + - `Item` — legacy 2.0-rc1 catalog item (backward compatible) + - `Resource` — new cross-domain resource abstraction (generalized) + Both types share the common `id` + `descriptor` surface for display rendering. + See `#/components/schemas/Catalog/v2.0` for the full Catalog schema. + items: + $ref: '#/components/schemas/Catalog' + + OnInitAction: + $id: https://schema.beckn.io/OnInitAction/v2.0 + title: On Init Action + description: | + Beckn /beckn/on_init message payload. Sent by a BPP to a BAP in response + to a /beckn/init call, with the updated contract including payment terms + and billing confirmation. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + required: [order] + properties: + order: + type: object + - type: object + required: [contract] + properties: + contract: + $ref: '#/components/schemas/Contract' + + OnRateAction: + $id: https://schema.beckn.io/OnRateAction/v2.0 + title: On Rate Action + description: | + Beckn /beckn/on_rate message payload. Sent by a BPP to a BAP in + response to a /beckn/rate call, optionally returning rating forms + to collect structured feedback from the consumer. + (Context wrapper stripped; only the message-content portion is inlined.) + type: object + additionalProperties: true + properties: + ratingForms: + type: array + items: + $ref: '#/components/schemas/RatingForm' + required: + - ratingForms + + # ── Support Messages ──────────────────────────────────────────── + + OnSelectAction: + $id: https://schema.beckn.io/OnSelectAction/v2.0 + title: On Select Action + description: | + Beckn /beckn/on_select message payload. Sent by a BPP to a BAP in + response to a /beckn/select call, with updated contract terms. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + - $ref: '#/components/schemas/Contract' + + OnStatusAction: + $id: https://schema.beckn.io/OnStatusAction/v2.0 + title: On Status Action + description: | + Beckn /beckn/on_status message payload. Sent by a BPP to a BAP in + response to a /beckn/status call (or as an unsolicited status push), + returning the current state of the contract. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + required: [order] + properties: + order: + type: object + - type: object + required: [contract] + properties: + contract: + $ref: '#/components/schemas/Contract' + + OnSupportAction: + $id: https://schema.beckn.io/OnSupportAction/v2.0 + title: On Support Action + description: | + Beckn /beckn/on_support message payload. Sent by a BPP to a BAP in + response to a /beckn/support call, returning support contact details + and available channels. + (Context wrapper stripped; only the message-content portion is inlined.) + type: object + properties: + support: + $ref: '#/components/schemas/Support' + required: + - support + + + # ── Catalog Publishing Messages ───────────────────────────────── + + OnTrackAction: + $id: https://schema.beckn.io/OnTrackAction/v2.0 + title: On Track Action + description: | + Beckn /beckn/on_track message payload. Sent by a BPP to a BAP in + response to a /beckn/track call, returning a Tracking handle with + the URL and/or WebSocket endpoint for real-time fulfillment tracking. + (Context wrapper stripped; only the message-content portion is inlined.) + type: object + additionalProperties: true + properties: + tracking: + allOf: + - $ref: '#/components/schemas/Tracking' + required: + - tracking + + OnUpdateAction: + $id: https://schema.beckn.io/OnUpdateAction/v2.0 + title: On Update Action + description: | + Beckn /beckn/on_update message payload. Sent by a BPP to a BAP in + response to a /beckn/update call (or as an unsolicited update push), + returning the updated contract state. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + required: [order] + properties: + order: + type: object + - type: object + required: [contract] + properties: + contract: + $ref: '#/components/schemas/Contract' + + + RateAction: + $id: https://schema.beckn.io/RateAction/v2.0 + title: RateAction + description: | + Beckn /beckn/rate message payload. Sent by a BAP to a BPP to submit + one or more rating inputs for entities in a completed contract + (order, fulfillment, item, provider, agent, support). + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + properties: + ratings: + type: array + items: + type: object + - type: object + additionalProperties: true + properties: + ratingInputs: + type: array + items: + $ref: '#/components/schemas/RatingInput' + required: + - ratingInputs + + + SelectAction: + $id: 'https://schema.beckn.io/SelectAction/v2.0' + title: Select Action + description: | + Beckn /beckn/select message payload. Sent by a BAP to a BPP to select + items and offers from a catalog, initiating the negotiation cycle. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + required: [order] + properties: + order: + type: object + - type: object + required: [contract] + properties: + contract: + $ref: '#/components/schemas/Contract' + + StatusAction: + $id: https://schema.beckn.io/StatusAction/v2.0 + title: Status Action + description: | + Beckn /beckn/status message payload. Sent by a BAP to a BPP to query + the current state of a contract/order by its identifier. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + description: Query by order identifier (legacy) + required: [order] + properties: + order: + allOf: + - type: object + - required: ['beckn:id'] + - type: object + description: Query by contract identifier + required: [contract] + properties: + contract: + type: object + required: [id] + properties: + id: + $ref: '#/components/schemas/Contract/properties/id' + + SupportAction: + $id: https://schema.beckn.io/SupportAction/v2.0 + title: Support Action + description: | + Beckn /beckn/support message payload. Sent by a BAP to a BPP to request + support contact information or to open a support ticket for an existing + order/contract. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + - type: object + properties: + support: + $ref: '#/components/schemas/Support' + required: + - support + + + TrackAction: + oneOf: + - type: object + - type: object + title: Track Action + description: | + Beckn /beckn/track message payload. Sent by a BAP to a BPP to request + a real-time tracking handle for a fulfillment within an active contract. + (Context wrapper stripped; only the message-content portion is inlined.) + properties: + tracking: + type: object + properties: + contract: + type: object + properties: + id: + $ref: '#/components/schemas/Contract/properties/id' + trackingData: + type: object + description: Domain-specific extension attributes for this track action. + properties: + '@context': + type: array + items: + type: string + format: uri + uniqueItems: true + contains: + const: 'https://schema.beckn.io/TrackAction/v2.0/context.jsonld' + minItems: 1 + '@type': + type: array + items: + type: string + format: uri + uniqueItems: true + contains: + const: 'beckn:TrackAction' + minItems: 1 + trackingAttributes: + description: Domain-specific extension attributes for this Tracking. + allOf: + - $ref: '#/components/schemas/Attributes' + - required: + - '@context' + - '@type' + - properties: + '@context': + description: Domain-specific context in which this contract is being created + type: string + format: uri + examples: + - 'https://schema.beckn.io/LongPolling/v2.0/context.jsonld' + - 'https://schema.beckn.io/WebhookPush/v2.0/context.jsonld' + - 'https://schema.beckn.io/Streaming/v2.0/context.jsonld' + - 'https://schema.beckn.io/WebPage/v2.0/context.jsonld' + '@type': + description: JSON-LD type corresponding to the context + type: string + examples: + - LongPolling + - WebhookPush + - Streaming + - WebPage + additionalProperties: true + required: + - contract + additionalProperties: false + + + # ── Status Message ────────────────────────────────────────────── + + UpdateAction: + $id: https://schema.beckn.io/UpdateAction/v2.0 + title: Update Action + description: | + Beckn /beckn/update message payload. Sent by a BAP to a BPP to request + changes to an active contract (e.g., update fulfillment address, add items, + change quantities). The context.try flag must be true during negotiation. + (Context wrapper stripped; only the message-content portion is inlined.) + oneOf: + - type: object + required: [order] + properties: + order: + type: object + - type: object + properties: + contract: + $ref: '#/components/schemas/Contract' + + # ─── Responses ──────────────────────────────────────────────────── + + # ───────────────────────────────────────────────────────────────────── + # CORE DATA SCHEMAS — Reusable types, domain objects, and infrastructure + # ───────────────────────────────────────────────────────────────────── + ActionTrigger: + description: Describes a trigger that initiates an action, like triggering a settlement on fulfillment completion. + type: object + properties: + '@context': + type: string + format: uri + '@type': + type: string + descriptor: + $ref: '#/components/schemas/Descriptor' + + AddOn: + type: object + description: Add-on to a catalog resource + properties: + id: + type: string + descriptor: + $ref: '#/components/schemas/Descriptor' + addOnAttributes: + description: > + Domain-specific extension attributes for this add-on. + allOf: + - $ref: '#/components/schemas/Attributes' + - required: + - '@context' + - '@type' + - properties: + '@context': + description: Domain-specific context in which this add-on exists + type: string + format: uri + examples: + - 'https://schema.beckn.io/FnbAddOn/v2.0/context.jsonld' + - 'https://schema.beckn.io/GroceryAddOn/v2.0/context.jsonld' + - 'https://schema.beckn.io/RideAddOn/v2.0/context.jsonld' + '@type': + description: JSON-LD type corresponding to the context + type: string + examples: + - FnbAddOn + - GroceryAddOn + - RideAddOn + Address: + $id: https://schema.beckn.io/Address/v2.0 + description: '**Postal address** aligned with schema.org `PostalAddress`. Use for human-readable addresses. + Geometry lives in `Location.geo` as GeoJSON.' + title: Address + type: object + properties: + addressCountry: + description: Country name or ISO-3166-1 alpha-2 code. + type: string + example: IN + addressLocality: + description: City/locality. + type: string + example: Bengaluru + addressRegion: + description: State/region/province. + type: string + example: Karnataka + extendedAddress: + description: Address extension (apt/suite/floor, C/O). + type: string + example: Apt 4B + postalCode: + description: Postal/ZIP code. + type: string + example: '560001' + streetAddress: + description: Street address (building name/number and street). + type: string + example: 123 Tech Street + additionalProperties: false + x-tags: + - common + + AsyncError: + title: Asynchronous Error + description: | + Error returned asynchronously during a callback. Wraps the base `Error` schema with JSON-LD type annotations to allow linked-data processing. + allOf: + - $ref: '#/components/schemas/Error' + - type: object + properties: + '@context': + type: string + default: '#/components/schemas/' + '@type': + type: string + default: AsyncError + additionalProperties: true + + Attributes: + $id: 'https://schema.beckn.io/Attributes/v2.0' + description: JSON-LD aware container for domain-specific attributes of an Item. MUST include @context + (URI) and @type (compact or full IRI). Any additional properties are allowed and interpreted per the + provided JSON-LD context. + title: Attributes + type: object + properties: + '@context': + description: Use case specific JSON-LD context URI + type: string + format: uri + '@type': + description: JSON-LD type defined within the context + type: string + required: + - '@context' + - '@type' + additionalProperties: true + x-tags: + - common + + Catalog: + $id: https://schema.beckn.io/Catalog/v2.1 + $schema: "https://json-schema.org/draft/2020-12/schema" + description: Catalog schema for Beckn Protocol v2.0.1 — oneOf legacy (v2.0) or new (v2.1) catalog format + title: Catalog + x-tags: [common] + type: object + properties: + bppId: + description: BPP (Beckn Protocol Provider) identifier that publishes this catalog + type: string + example: bpp.example.com + bppUri: + description: BPP (Beckn Protocol Provider) URI endpoint + type: string + format: uri + example: "https://bpp.example.com" + descriptor: + description: Human / Agent-readable description of this catalog + allOf: + - $ref: "#/components/schemas/Descriptor" + id: + description: Unique identifier for the catalog + type: string + example: catalog-electronics-001 + isActive: + description: Whether the catalog is active + type: boolean + default: true + offers: + type: array + items: + $ref: "#/components/schemas/Offer" + resources: + description: Array of generalized Resource entities in this catalog (new model) + type: array + items: + $ref: '#/components/schemas/Resource' + provider: + description: The provider of the catalog + allOf: + - $ref: "#/components/schemas/Provider" + visibleTo: + type: array + description: > + Optional visibility constraint indicating which network participants + (by participantId / networkId / role) are allowed to discover or + transact on this catalog. + + If omitted, the catalog is assumed to be visible to all participants + in the addressed network(s). + items: + type: object + required: [type, id] + properties: + type: + type: string + enum: [NETWORK, PARTICIPANT, ROLE] + description: Scope of visibility constraint. + id: + type: string + description: Identifier of the network, participant, or role. + validity: + description: The time period during which this catalog is valid + $ref: "#/components/schemas/TimePeriod" + required: + - id + - descriptor + additionalProperties: false + + CatalogOnPublishMessage: + $id: 'https:schema.beckn.io/CatalogOnPublishMessage/v2.0' + description: Catalog publish processing results from CDS to BPP. + type: object + required: [results] + properties: + results: + type: array + description: Per-catalog processing results + items: + $ref: '#/components/schemas/CatalogProcessingResult' + + CatalogProcessingResult: + $id: 'https://schema.beckn.io/CatalogProcessingResult/v2.0' + description: Processing result for a single catalog submission. + type: object + required: [catalogId, status] + properties: + catalogId: + type: string + description: Identifier of the submitted catalog + status: + description: | + Processing outcome. Using oneOf [string, object] to allow domain-specific + status objects (e.g. beckn:CatalogAccepted) alongside standard string codes. + oneOf: + - type: string + enum: [ACCEPTED, REJECTED, PARTIAL] + description: | + Standard processing status: + - ACCEPTED — catalog indexed successfully + - REJECTED — catalog rejected entirely (see errors) + - PARTIAL — some items accepted, some rejected (see errors) + - type: object + properties: + '@context': + type: string + format: uri + const: '#/components/schemas/' + '@type': + type: string + pattern: '^beckn:[A-Za-z0-9._~-]+$' + additionalProperties: true + errors: + type: array + description: Per-item or per-catalog errors (present when REJECTED or PARTIAL) + items: + $ref: '#/components/schemas/Error' + stats: + type: object + description: Optional statistics about the processed catalog + properties: + itemCount: + type: integer + description: Number of items accepted + providerCount: + type: integer + description: Number of providers in the catalog + categoryCount: + type: integer + description: Number of distinct categories + + Consideration: + $id: https://schema.beckn.io/Consideration/v2.0 + type: object + description: > + Generalized representation of value exchanged under a Contract. + + Consideration is domain-neutral and may represent: + - Monetary value + - Credits / tokens + - Asset transfer + - Service exchange + - Compliance artifact + required: ["@context", "@type", "id", "status"] + properties: + "@context": + type: string + format: uri + description: JSON-LD context URI for the core Resource schema + "@type": + oneOf: + - type: string + description: Type of the core Resource + enum: ["beckn:Consideration", "beckn:MonetaryConsideration", "beckn:TokenConsideration", "beckn:CreditConsideration", "beckn:AssetConsideration", "beckn:ServiceConsideration"] + - type: string + id: + description: Identifier of this consideration + type: string + status: + description: The status of this consideration + $ref: '#/components/schemas/Descriptor' + considerationAttributes: + description: > + Domain-specific attributes of this consideration. For monetary considerations, + use the PriceSpecification schema to capture total value with breakup. + For other consideration types, use a generic Attributes bag. + $ref: '#/components/schemas/Attributes' + + Context: + $id: "#/components/schemas/Context" + description: >- + Every API call in Beckn protocol has a context. It contains addressing information like the bap/bpp identifiers and API base urls, the protocol version implemented by the sender, the method being called, the transaction_id that represents an end-to-end user session at the BAP, a message ID to pair requests with callbacks, a timestamp to capture sending times, a ttl to specify the validity of the request, and a key to encrypt information if necessary. + title: Context + x-tags: [common] + type: object + properties: + action: + description: The Beckn endpoint being called. Must conform to BecknEndpoint format. + type: string + bapId: + description: A globally unique identifier of the BAP. Typically the fully qualified domain name (FQDN) of the BAP. + type: string + bapUri: + description: API URL of the BAP for accepting callbacks from BPPs. + type: string + format: uri + bppId: + description: A globally unique identifier of the BPP. Typically the fully qualified domain name (FQDN) of the BPP. + type: string + bppUri: + description: API URL of the BPP for accepting calls from BAPs. + type: string + format: uri + messageId: + description: >- + A unique value which persists during a request/callback cycle. BAPs use + this value to match an incoming callback from a BPP to an earlier call. + Generate a fresh message_id for every new interaction. + type: string + format: uuid + networkId: + description: A unique identifier representing a group of platforms. By default, the URL of the network registry on the Beckn network. + type: string + timestamp: + description: Time of request generation in RFC3339 format. + type: string + format: date-time + transactionId: + description: >- + A unique value which persists across all API calls from discover through + confirm. Used to indicate an active user session across multiple requests. + type: string + format: uuid + try: + description: A flag to indicate that this request is intended to try an operation in sandbox mode + type: boolean + default: false + ttl: + description: The duration in ISO8601 format after timestamp for which this message holds valid. + type: string + version: + description: Version of Beckn protocol being used by the sender. + type: string + default: 2.0.0 + required: + - action + - bapId + - bapUri + - messageId + - timestamp + - transactionId + - version + additionalProperties: false + + Contract: + $id: https://schema.beckn.io/Contract/v2.1 + type: object + description: > + This is a JSON-LD compliant, linked-data schema that specifies a generic multi-party, digitally signed Contract between a set of participants. based on the vocabulary defined in the @context. By default, it is the most generic form of contract i.e beckn:Contract. However, based on the mapping being used in @context, it could take values like retail:Order, mobility:Reservation, healthcare:Appointment, and so on, which will be defined as sub-classes of beckn:Contract. + Alternate description A digitally agreed commitment between two or more participants + governing the exchange of economic or non-economic value. + + Contract is the canonical contract object in the generalized + Beckn v2.1 protocol. It replaces the commerce-specific Order construct + while preserving backward compatibility at the API layer. + + A Contract binds: + - Commitments (what is agreed) + - Consideration (value promised) + - Performance (how execution occurs) + - Settlements (how consideration is discharged) + + The model is domain-neutral and supports commerce, hiring, + energy markets, carbon exchanges, data access, mobility, + subscriptions, and other use cases. + title: Contract + properties: + '@context': + description: A URL to the reference vocabulary where this schema has been defined. If missing, this field defaults to `https://schema.beckn.io/`. It allows applications to fetch the mapping between the simple JSON keys of this class and absolute Beckn IRIs (Internationalized Resource Identifiers), allowing conversion of standard Beckn JSON payload into linked data. + type: string + format: uri + const: https://schema.beckn.io/Contract/v2.0 + '@type': + description: A Beckn IRI on the vocabulary defined in the @context. Must start with "beckn:" followed by URL-safe identifier characters. + type: string + pattern: ^beckn:[A-Za-z0-9._~-]+$ + default: beckn:Contract + id: + description: A UUID string generated at the BPP endpoint at any stage before the confirmation of the order i.e before `/on_confirm` callback. This value is intended typically for indexing or filtering. While the chances of a UUID collision are rare, it is recommended to use a combination of `bppId`, `providerId` and `id` to allow for global uniqueness. + type: string + format: uuid + descriptor: + description: Describes the nature of the contract in human / agent readable terms + $ref: '#/components/schemas/Descriptor' + commitments: + type: array + description: > + Structured commitments governed by this contract. + minItems: 1 + items: + $ref: '#/components/schemas/Commitment' + consideration: + type: array + description: Value agreed to be exchanged under this contract. + items: + $ref: '#/components/schemas/Consideration' + participants: + description: | + The participants involved in the contract. Contracts are not always between two individuals. + + Several entities may play a specific role in the creation, fulfillment, and post-fulfillment of + the contract. + type: array + items: + $ref: '#/components/schemas/Participant' + performance: + type: array + description: > + Execution units of the contract. + + Previously defined as `fulfillments`, performance is the generalized the legacy Fulfillment abstraction. + Each Performance instance represents a structured execution + plan or delivery mechanism, including: + + - Physical delivery + - Service provisioning + - API access + - Subscription activation + - Carbon credit transfer + - Capacity allocation + - Workforce onboarding + + Tracking and Support interactions may be linked to + individual Performance units. + items: + $ref: '#/components/schemas/Performance' + entitlements: + type: array + items: + $ref: '#/components/schemas/Entitlement' + settlements: + type: array + description: > + Records representing discharge of agreed consideration. + items: + $ref: '#/components/schemas/Settlement' + terms: + type: array + description: > + Records representing discharge of agreed consideration. + items: + $ref: '#/components/schemas/Attributes' + status: + description: The current state of the contract expressed as a Descriptor whose code MUST be one of the standard contract state values. + allOf: + - $ref: '#/components/schemas/Descriptor' + - type: object + properties: + code: + enum: [DRAFT, ACTIVE, CANCELLED, COMPLETE] + contractAttributes: + description: > + Domain-specific extension attributes for this contract. + Use beckn:LogisticsContract for hyperlocal physical delivery. Use the generic Attributes schema for + other fulfillment types where no well-known domain schema exists. + allOf: + - $ref: '#/components/schemas/Attributes' + - required: + - '@context' + - '@type' + - properties: + '@context': + description: Domain-specific context in which this contract is being created + type: string + format: uri + examples: + - 'https://schema.beckn.io/GroceryOrder/v2.0/context.jsonld' + - 'https://schema.beckn.io/FnBOrder/v2.0/context.jsonld' + - 'https://schema.beckn.io/ShippingOrder/v2.0/context.jsonld' + - 'https://schema.beckn.io/MobilityBooking/v2.0/context.jsonld' + - 'https://schema.beckn.io/BusTicketBooking/v2.0/context.jsonld' + - 'https://schema.beckn.io/P2PEnergyTrade/v1.1/context.jsonld' + '@type': + description: JSON-LD type corresponding to the context + type: string + examples: + - GroceryOrder + - FnBOrder + - ShippingOrder + - MobilityBooking + - BusTicketBooking + - P2PEnergyTrade + + additionalProperties: false + x-tags: + - common + required: + - commitments + + Commitment: + type: object + required: + - status + - resources + - offer + properties: + "@context": + type: string + format: uri + description: JSON-LD context URI for the core Resource schema + "@type": + type: string + description: Type of the core Resource + enum: ["beckn:Commitment"] + id: + type: string + status: + type: object + properties: + descriptor: + allOf: + - $ref: '#/components/schemas/Descriptor' + - properties: + code: + enum: + - DRAFT + - ACTIVE + - CLOSED + resources: + type: array + items: + required: + - id + - quantity + $ref: '#/components/schemas/Resource' + offer: + required: + - id + - resourceIds + $ref: '#/components/schemas/Offer' + commitmentAttributes: + description: > + Domain-specific extension attributes for this commitment. + allOf: + - $ref: '#/components/schemas/Attributes' + - required: + - '@context' + - '@type' + properties: + '@context': + description: Domain-specific attributes for this commitment + type: string + format: uri + examples: + - 'https://schema.beckn.io/ShoppingCart/v2.0/context.jsonld' + - 'https://schema.beckn.io/ShippingManifest/v2.0/context.jsonld' + - 'https://schema.beckn.io/TravelItinerary/v2.0/context.jsonld' + - 'https://schema.beckn.io/BillOfMaterials/v2.0/context.jsonld' + - 'https://schema.beckn.io/DiagnosticTests/v2.0/context.jsonld' + - 'https://schema.beckn.io/TodoList/v2.0/context.jsonld' + '@type': + description: JSON-LD type corresponding to the context + type: string + examples: + - ShoppingCart + - ShippingManifest + - TravelItinerary + - BillOfMaterials + - DiagnosticTests + - TodoList + + CounterSignature: + title: Beckn HTTP Counter-Signature + description: | + A signed receipt transmitted in the synchronous `Ack` response body, proving that the + receiver authenticated, received, and processed the inbound request. + + `CounterSignature` shares the same wire format as `Signature` but differs: + - **Signer**: the response receiver (not the request sender) + - **Location**: transmitted in the `Ack` response body (not in the `Authorization` header) + - **`digest`**: covers the Ack response body (not the inbound request body) + - **`(request-digest)`** and **`(message-id)`** MUST be included in the signing string + + Signing string format: + ``` + (created): {unixTimestamp} + (expires): {unixTimestamp} + digest: BLAKE-512={base64DigestOfAckBody} + (request-digest): BLAKE-512={base64DigestOfInboundRequestBody} + (message-id): {messageId} + ``` + allOf: + - $ref: '#/components/schemas/Signature' + + Descriptor: + $id: "#/components/schemas/Descriptor" + $schema: "https://json-schema.org/draft/2020-12/schema" + description: Schema definition for Descriptor in the Beckn Protocol v2.0.1 + title: Descriptor + x-tags: [common] + type: object + properties: + '@context': + description: Use case specific JSON-LD context. This can change from use case to use case, even within a domain. + type: string + format: uri + default: "https://schema.beckn.io/" + '@type': + description: Type of the descriptor. The type can be overriden with a context-specific type + type: string + default: beckn:Descriptor + code: + description: A machine-readable code identifying the state or type of the entity being described. The valid values for this field are defined by the context in which the Descriptor is used (e.g. DRAFT, ACTIVE, CANCELLED, COMPLETE for a Contract status). + type: string + longDesc: + description: Detailed description of the item + type: string + example: Powerful gaming laptop with NVIDIA RTX graphics, fast SSD storage, and high-refresh display + shortDesc: + description: Short description of the item + type: string + example: High-performance gaming laptop with RTX graphics + name: + description: Name of the entity being described + type: string + example: Premium Gaming Laptop Pro + thumbnailImage: + description: Name of the entity being described + type: string + format: uri + docs: + description: Links to downloadable documents + type: array + items: + type: object + mediaFile: + description: Links to multimedia files and images + type: array + items: + type: object + required: + - '@type' + additionalProperties: false + + Entitlement: + $id: https://schema.beckn.io/Entitlement/v2.0 + description: A contractually granted, policy-governed right that allows a specific party to access, use, or claim a defined economic resource within stated scope and validity constraints. It represents the enforceable permission created by an order, independent of the credential used to exercise it. + title: Entitlement + type: object + properties: + '@context': + description: CPD + type: string + format: uri + default: https://schema.beckn.io/ + '@type': + description: The domain-specific type of entitlement that allows domains to extend this schema + type: string + default: beckn:Entitlement + id: + description: A unique identifier for this entitlement within the entitlement provider's namespace + type: string + resource: + description: The resource being availed or accessed against this entitlement + $ref: '#/components/schemas/Resource/properties/id' + descriptor: + description: Human-readable information regarding the entitlement like QR-code images, attached documents containing terms and conditions, video or audio files instructing the user on how to use the entitlement + $ref: '#/components/schemas/Descriptor' + credentials: + type: array + items: + $ref: '#/components/schemas/Descriptor' + additionalProperties: true + required: + - '@context' + - '@type' + - id + - descriptor + x-tags: + - common + + Error: + $id: '#/components/schemas/Error/v2.0' + title: Error + description: | + Base error container returned in NACK responses and async error callbacks. + Domain-specific error codes SHOULD follow the Beckn error code registry + (see docs/11_Error_Codes.md). Custom error objects may be expressed as + `oneOf [string, object]` payloads via the `errorDetails` field. + oneOf: + - description: Newest version of the error object properties. + type: object + properties: + errorCode: + type: string + description: Machine-readable error code per the Beckn error registry. + errorMessage: + type: string + description: Human-readable error description for display or logging. + errorDetails: + description: | + Optional domain-specific error details. Using oneOf to allow both simple + string details and structured JSON-LD error objects from domain packs. + oneOf: + - type: string + - type: object + additionalProperties: true + - description: Error response object from the previous version + type: object + required: [code, message] + properties: + code: + type: string + message: + type: string + details: + type: object + Participant: + type: object + properties: + id: + type: string + descriptor: + $ref: '#/components/schemas/Descriptor' + participantAttributes: + description: > + Domain-specific extension attributes for this contract. + Use beckn:LogisticsContract for hyperlocal physical delivery. Use the generic Attributes schema for + other fulfillment types where no well-known domain schema exists. + allOf: + - $ref: '#/components/schemas/Attributes' + - required: + - '@context' + - '@type' + properties: + '@context': + description: Domain-specific entity which is is participating in this transaction + type: string + format: uri + examples: + - 'https://schema.beckn.io/Consumer/v2.0/context.jsonld' + - 'https://schema.beckn.io/Provider/v2.0/context.jsonld' + - 'https://schema.beckn.io/FulfillmentAgent/v2.0/context.jsonld' + - 'https://schema.beckn.io/NetworkParticipant/v2.0/context.jsonld' + - 'https://schema.beckn.io/EscrowAgent/v2.0/context.jsonld' + - 'https://schema.beckn.io/Guarantor/v2.0/context.jsonld' + '@type': + description: JSON-LD type corresponding to the context + type: string + examples: + - Consumer + - Provider + - FulfillmentAgent + - NetworkParticipant + - EscrowAgent + - Guarantor + + GeoJSONGeometry: + $id: 'https://schema.beckn.io/GeoJSONGeometry/v2.0' + description: "**GeoJSON geometry** per RFC 7946. Coordinates are in **EPSG:4326 (WGS-84)** and MUST follow\ + \ **[longitude, latitude, (altitude?)]** order.\nSupported types: - Point, LineString, Polygon - MultiPoint,\ + \ MultiLineString, MultiPolygon - GeometryCollection (uses `geometries` instead of `coordinates`)\n\ + Notes: - For rectangles, use a Polygon with a single linear ring where the first\n and last positions\ + \ are identical.\n- Circles are **not native** to GeoJSON. For circular searches, use\n `SpatialConstraint`\ + \ with `op: s_dwithin` and a Point + `distanceMeters`,\n or approximate the circle as a Polygon.\n\ + - Optional `bbox` is `[west, south, east, north]` in degrees.\n" + title: GeoJSONGeometry + type: object + properties: + bbox: + description: Optional bounding box `[west, south, east, north]` in degrees. + type: array + minItems: 4 + maxItems: 4 + coordinates: + description: Coordinates per RFC 7946 for all types **except** GeometryCollection. Order is **[lon, + lat, (alt)]**. For Polygons, this is an array of linear rings; each ring is an array of positions. + type: array + geometries: + description: Member geometries when `type` is **GeometryCollection**. + type: array + items: + $ref: '#/components/schemas/GeoJSONGeometry' + type: + type: string + enum: + - Point + - LineString + - Polygon + - MultiPoint + - MultiLineString + - MultiPolygon + - GeometryCollection + required: + - type + additionalProperties: true + x-tags: + - common + + Performance: + $id: "https://schema.beckn.io/Performance/v2.2" + description: | + Generalized execution/performance unit. This is where downstream objects bind: + - Fulfillment-like details (where/when/how) + - Tracking handles + - Support touchpoints + - Status updates + + A minimal envelope that carries identity, status, and a performanceAttributes + bag that holds the concrete domain-specific delivery schema. + + Domain specification authors can attach rich context and types via `performanceAttributes`. + + For example, Hyperlocal delivery details (pickup/delivery locations, items shipped, agent, etc.) + are placed inside performanceAttributes using a well-known domain schema such as + HyperlocalDelivery. Use the generic Attributes schema when no well-known + domain schema exists. + title: Fulfillment + x-tags: [common, fulfillment] + type: object + properties: + '@context': + description: Domain-specific context in which this fulfillment is taking place + type: string + format: uri + const: 'https://schema.beckn.io/Performance/v2.1/context.jsonld' + '@type': + description: JSON-LD type. + type: string + default: 'Fulfillment' + id: + type: string + description: Unique identifier for this fulfillment record. + x-jsonld: + '@id': 'beckn:id' + status: + $ref: '#/components/schemas/Descriptor' + description: > + Current status of this fulfillment, expressed as a Descriptor. + Use Descriptor.code for machine-readable status values. + x-jsonld: + '@id': 'beckn:status' + commitmentIds: + type: array + items: + $ref: '#/components/schemas/Commitment/properties/id' + schedule: + $ref: '#/components/schemas/Schedule' + performanceAttributes: + description: > + Domain-specific extension attributes for this fulfillment. + Use beckn:HyperlocalDelivery (aligned with schema:ParcelDelivery) for + hyperlocal physical delivery. Use the generic Attributes schema for + other fulfillment types where no well-known domain schema exists. + allOf: + - $ref: '#/components/schemas/Attributes' + - properties: + '@context': + description: Domain-specific context in which this fulfillment is taking place + type: string + format: uri + examples: + - 'https://schema.beckn.io/HyperlocalDelivery/v2.0/context.jsonld' + - 'https://schema.beckn.io/OnDemandRide/v2.0/context.jsonld' + - 'https://schema.beckn.io/P2PEnergyTransfer/v1.1/context.jsonld' + default: 'https://schema.beckn.io/Fulfillment/v2.1/context.jsonld' + '@type': + description: JSON-LD type. + type: string + examples: + - 'HyperlocalDelivery' + - 'OnDemandRide' + - 'P2PEnergyTransfer' + - 'Fulfillment' + required: + - '@context' + - '@type' + additionalProperties: false + + InReplyTo: + $id: 'https://schema.beckn.io/InReplyTo/v2.0' + title: In-Reply-To Reference + description: | + A cryptographic binding that ties an asynchronous `on_` callback to the + specific inbound request that triggered it. Establishes bilateral non-repudiation + for the asynchronous leg of a Beckn interaction. + + Use `lineage` (on `Context`) for cross-transaction causality. + + Verification steps: + 1. Recompute BLAKE2b-512 digest of the original request body; compare to `digest`. + 2. Confirm `messageId` matches the `messageId` from the original request `Context`. + type: object + required: [messageId, digest] + properties: + messageId: + type: string + format: uuid + description: The `messageId` of the parent request from its `Context`. + digest: + type: string + pattern: '^BLAKE-512=[A-Za-z0-9+/]+=*$' + description: | + BLAKE2b-512 hash of the parent request body, Base64-encoded with algorithm prefix. + Format: `BLAKE-512={base64EncodedHash}` + + Intent: + $id: https://schema.beckn.io/Intent/v2.0 + description: A declaration of an intent to transact + title: Intent + x-tags: [common] + type: object + properties: + textSearch: + description: Free text search query for items + type: string + example: gaming laptop premium tech + filters: + description: Filter criteria for items + type: object + properties: + type: + description: Type of filter expression + type: string + enum: + - jsonpath + default: jsonpath + expression: + description: Filter expression based on the specified type + type: string + example: $[?(@.rating.value >= 4.0 && @.electronic.brand.name == 'Premium Tech')] + required: + - type + - expression + spatial: + description: Optional array of spatial constraints (CQL2-JSON semantics). + type: array + items: + type: object + media_search: + $ref: "#/components/schemas/MediaSearch" + anyOf: + - required: + - textSearch + - required: + - filters + - required: + - spatial + - required: + - filters + - spatial + additionalProperties: false + + + Location: + description: | + A place represented by GeoJSON geometry and optional address. + Source: main/schema/core/v2/attributes.yaml#Location + type: object + required: [geo] + additionalProperties: false + properties: + "@type": + type: string + enum: ["beckn:Location"] + geo: + $ref: '#/components/schemas/GeoJSONGeometry' + address: + oneOf: + - type: string + - $ref: '#/components/schemas/Address' + + MediaInput: + $id: https://schema.beckn.io/MediaInput/v2.0 + type: object + required: [type, url] + properties: + id: { type: string } + type: { type: string, enum: [image, audio, video] } + url: { type: string, format: uri } + contentType: { type: string } + textHint: { type: string } + language: { type: string } + startMs: { type: integer } + endMs: { type: integer } + additionalProperties: false + + MediaSearch: + $id: https://schema.beckn.io/MediaSearch/v2.0 + type: object + properties: + media: + type: array + items: + $ref: '#/components/schemas/MediaInput' + options: + $ref: '#/components/schemas/MediaSearchOptions' + additionalProperties: false + + MediaSearchOptions: + $id: https://schema.beckn.io/MediaSearchOptions/v2.0 + type: object + properties: + goals: + type: array + items: + type: string + enum: [visual-similarity, visual-object-detect, text-from-image, text-from-audio, semantic-audio-match, semantic-video-match] + augment_text_search: + type: boolean + default: true + restrict_results_to_media_proximity: + type: boolean + default: false + additionalProperties: false + + Offer: + $id: 'https://schema.beckn.io/Offer/v2.1' + type: object + description: | + A generalized, cross-domain Offer that captures the terms under which + one or more Resources may be committed. + + Core intent: + - Support multiple terms/eligibility/constraints/price points for the same Resource(s) + - Support dynamic / on-the-fly offers (e.g., bundling, combinational discounts, + eligibility changes, capacity-aware pricing) + + This mirrors the role of Offer in current Beckn (and schema.org patterns), + but keeps the shape minimal and composable via `beckn:offerAttributes`. + required: ["@context", "@type", "id"] + properties: + "@context": + type: string + format: uri + description: JSON-LD context URI for the core Offer schema + enum: ["https://schema.beckn.io/Offer/v2.2/context.jsonld"] + "@type": + type: string + description: Type of the core Offer + enum: ["beckn:Offer"] + id: + type: string + description: Unique identifier of the offer. + descriptor: + description: Human / agent-readable description of this offer. + $ref: '#/components/schemas/Descriptor' + provider: + description: > + Provider of this offer. + - If the offer is published by the same provider as the item it is associated with, + use only the provider's `id` (string reference). + - If the offer is published by a different provider, include the full Provider object. + oneOf: + - type: object + description: Same provider as the item — identified by id only. + required: [id] + properties: + id: + type: string + description: Identifier of the provider (same as the item's provider). + - $ref: '#/components/schemas/Provider' + resourceIds: + type: array + description: References (IDs) to resources covered by this offer. + items: + $ref: '#/components/schemas/Resource/properties/id' + addOnIds: + type: array + description: IDs of optional extra Offers or Resources that can be attached. + items: + $ref: '#/components/schemas/AddOn/properties/id' + considerationIds: + type: array + items: + $ref: '#/components/schemas/Consideration/properties/id' + fulfillmentIds: + description: Details regarding the fulfillment of this offer + $ref: '#/components/schemas/Consideration' + validity: + $ref: '#/components/schemas/TimePeriod' + availableTo: + type: array + description: > + Optional visibility constraint indicating which network participants + (by participantId / networkId / role) are allowed to discover or + transact on this entity. + + If omitted, the entity is assumed to be visible to all participants + in the addressed network(s). + items: + type: object + required: [type, id] + properties: + type: + type: string + enum: [NETWORK, PARTICIPANT, ROLE] + description: Scope of visibility constraint. + id: + type: string + description: Identifier of the network, participant, or role. + offerAttributes: + $ref: '#/components/schemas/Attributes' + + + PriceSpecification: + type: object + properties: + priceUnit: + type: string + consideredValue: + type: number + components: + type: array + items: + type: object + required: + - lineId + - lineSummary + - value + - currency + properties: + lineId: + type: string + lineSummary: + type: string + value: + type: number + currency: + type: string + commitmentId: + $ref: '#/components/schemas/Commitment/properties/id' + quantity: + $ref: '#/components/schemas/Quantity' + + ProcessingNotice: + $id: https://schema.beckn.io/ProcessingNotice/v2.0 + type: object + required: [code, message] + properties: + code: + type: string + message: + type: string + details: + type: object + additionalProperties: true + + + Provider: + type: object + description: Schema definition for Provider in the Beckn Protocol v2.0.1 + title: Provider + x-tags: [common] + properties: + '@context': + type: string + format: uri + default: "https://schema.beckn.io/" + '@type': + type: string + const: beckn:Provider + descriptor: + $ref: "#/components/schemas/Descriptor" + id: + description: Unique identifier for the provider + type: string + example: tech-store-001 + locations: + description: Physical locations where the provider operates + type: array + items: + $ref: "#/components/schemas/Location" + policies: + type: array + items: + type: object + providerAttributes: + $ref: "#/components/schemas/Attributes" + required: + - id + - descriptor + additionalProperties: false + + Quantity: + $id: https://schema.beckn.io/Quantity/v2.0 + description: Schema definition for Quantity in the Beckn Protocol v2.0.1 + title: Quantity + type: object + properties: + maxQuantity: + description: Maximum quantity for this price + type: number + x-jsonld: + '@id': schema:Number + example: 100 + minQuantity: + description: Minimum quantity for this price + type: number + x-jsonld: + '@id': schema:Number + example: 1 + unitCode: + description: Unit code for the quoted price (e.g., KWH, MIN, H, MON) + type: string + x-jsonld: + '@id': schema:unitCode + example: KWH + unitQuantity: + description: Quantity of the unit + type: number + x-jsonld: + '@id': schema:Number + example: 1 + unitText: + description: Unit for the quoted price (e.g., kWh, minute, hour, month) + type: string + x-jsonld: + '@id': schema:unitText + example: kWh + x-jsonld: + '@id': schema:QuantitativeValue + additionalProperties: false + x-tags: + - common + RatingForm: # Move to schema.beckn.io/RatingForm/v2.0 + $id: https://schema.beckn.io/RatingForm/v2.0 + $schema: "https://json-schema.org/draft/2020-12/schema" + description: A form designed to capture rating and feedback from a user. This can be used by both BAP and BPP to fetch ratings + and feedback of their respective users. + title: RatingForm + x-tags: [common] + type: object + properties: + '@context': + type: string + format: uri + const: "https://schema.beckn.io/" + '@type': + type: string + default: RatingForm + x-jsonld: + '@id': beckn:RatingForm + target: + description: The entity being rated + type: object + properties: + '@context': + type: string + format: uri + const: "https://schema.beckn.io/" + '@type': + type: string + default: RatingTarget + x-jsonld: + '@id': beckn:RatingTarget + id: + description: ID of the entity being rated (order/item/fulfillment/provider/agent). + type: string + ratingTargetType: + description: What is being rated. + type: string + enum: [Order, Fulfillment, FulfillmentStage, Provider, Agent] + range: + oneOf: + - type: object + properties: + bestRating: + description: Maximum of the rating scale (default 5 if omitted). + type: number + worstRating: + description: Minimum of the rating scale (default 1 or 0 if omitted). + type: number + - type: array + description: A custom rating range indexed from 0 to n + items: + type: object + properties: + index: + type: integer + minimum: 0 + value: + $ref: "#/components/schemas/Descriptor" + feedbackForm: + description: A feedback form sent along with a rating request + type: object + feedbackRequired: + type: boolean + description: Specifies whether feedback after rating is required for acceptance of rating + required: + - '@context' + - '@type' + - target + - feedbackRequired + additionalProperties: false + + RatingInput: + $id: https://schema.beckn.io/RatingInput/v2.0 + title: RatingInput + description: | + A rating input form. Extends FormInput with the @type fixed to beckn:RatingForm, + indicating that this form instance is specifically a rating form. + allOf: + - type: object + - type: object + properties: + '@type': + type: string + const: 'beckn:RatingForm' + + Rating: + $id: https://schema.beckn.io/Rating + oneOf: + - $ref: '#/components/schemas/Rating' + - title: Rating + type: object + description: Aggregated rating information for an entity. Aligns with schema.org/AggregateRating semantics. + properties: + '@context': + type: string + format: uri + const: https://schema.beckn.io/ + '@type': + type: string + default: beckn:Rating + minValue: + description: Rating value (typically 0-5) + type: number + maxValue: + description: Number of ratings + type: integer + minimum: 0 + x-jsonld: + '@id': schema:ratingCount + reviewText: + description: Optional textual review or comment + type: string + x-jsonld: + '@id': schema:reviewBody + additionalProperties: false + x-tags: + - common + + Resource: + $id: 'https://schema.beckn.io/Resource/v2.0' + type: object + description: > + A minimal, domain-neutral abstraction representing any discoverable, + referenceable, or committable unit of value, capability, service, + entitlement, or asset within the network. + + Examples: + - A retail product SKU, a mobility ride, a job role, a carbon credit unit, + a dataset/API entitlement, a training course, a clinic service slot. + + Designed for composability through `resourceAttributes` where + domain packs can plug in their specific fields without changing the core. + required: ["@context", "@type", "id", "descriptor"] + properties: + "@context": + type: string + format: uri + description: JSON-LD context URI for the core Resource schema + "@type": + type: string + description: Type of the core Resource + const: "beckn:Resource" + id: + type: string + description: Globally unique identifier of the resource. + descriptor: + $ref: '#/components/schemas/Descriptor' + resourceAttributes: + description: All the properties of a resource that describe its value, its terms of usage, fulfillment, and consideration + $ref: '#/components/schemas/Attributes' + + Settlement: + $id: 'https://schema.beckn.io/Settlement/v2.0' + type: object + properties: + "@context": + type: string + format: uri + description: The domain-specific context in which this settlement is taking place. + examples: + - https://schema.beckn.io/Settlement/2.0/context.jsonld + - https://schema.beckn.io/RetailOrderSettlement/2.0/context.jsonld + - https://schema.beckn.io/P2PTradeSettlement/context.jsonld + "@type": + type: string + description: Type of settlement within the context of this settlement + examples: + - "beckn:Settlement" + - "beckn:MonetaryTransfer" + - "beckn:TokenTransfer" + - "beckn:CreditSettlement" + - "beckn:AssetTransfer" + - "beckn:ServiceSettlement" + default: "beckn:MonetaryTransfer" + id: + type: string + considerationId: + $ref: '#/components/schemas/Consideration/properties/id' + status: + type: string + enum: + - DRAFT + - COMMITTED + - COMPLETE + settlementTerms: + type: array + items: + $ref: '#/components/schemas/SettlementTerm' + settlementActions: + type: array + items: + $ref: '#/components/schemas/SettlementAction' + settlementAttributes: + $ref: '#/components/schemas/Attributes' + + + SettlementAction: + $id: 'https://schema.beckn.io/SettlementAction/v2.0' + type: object + properties: + settlementTermId: + $ref: '#/components/schemas/SettlementTerm/properties/id' + status: + $ref: '#/components/schemas/Descriptor' + from: + $ref: '#/components/schemas/Participant' + to: + $ref: '#/components/schemas/Participant' + method: + $ref: '#/components/schemas/SettlementMethod' + instrument: + $ref: '#/components/schemas/SettlementInstrument' + + SettlementInstrument: + type: object + properties: + '@context': + type: string + '@type': + type: string + id: + type: string + descriptor: + $ref: '#/components/schemas/Descriptor' + + SettlementMethod: + type: object + properties: + '@context': + type: string + '@type': + type: string + id: + type: string + descriptor: + $ref: '#/components/schemas/Descriptor' + + + SettlementTerm: + $id: 'https://schema.beckn.io/SettlementTerm/v2.0' + type: object + properties: + id: + type: string + committed: + type: boolean + default: false + commitments: + type: array + items: + type: object + properties: + id: + $ref: '#/components/schemas/Commitment/properties/id' + required: + - id + performance: + required: + - id + oneOf: + - type: object + properties: + id: + $ref: '#/components/schemas/Performance/properties/id' + - $ref: '#/components/schemas/Performance' + type: string + trigger: + $ref: '#/components/schemas/ActionTrigger' + eligibleInstrument: + $ref: '#/components/schemas/SettlementInstrument' + schedule: + $ref: '#/components/schemas/Schedule' + from: + $ref: '#/components/schemas/Participant' + to: + $ref: '#/components/schemas/Participant' + eligibleMethods: + type: array + items: + $ref: '#/components/schemas/SettlementMethod' + settlementTermAttributes: + $ref: '#/components/schemas/Attributes' + + Schedule: + $id: 'https://schema.beckn.io/Schedule/v2.0' + title: Schedule + description: > + A schedule defines a repeating time period used to describe a regularly + occurring Event. At a minimum a schedule will specify repeatFrequency which + describes the interval between occurrences of the event. Additional information + can be provided to specify the duration of occurrences, an exception to the + schedule (exceptDate), and an end condition (either numberOfRepeats, repeatCount + or endDate). + + Modeled after https://schema.org/Schedule. + type: object + properties: + byDay: + description: > + Defines the day(s) of the week on which a recurring Event takes place. + More specifically, to specify the day(s) of the week, use DayOfWeek; + for daily recurrence, use schema:DayOfWeek/Everyday. + A string value may also be used, for example "Sunday", "MO" (for Monday), or "MO TU" (for Monday and Tuesday). + oneOf: + - type: string + description: A day-of-week string (e.g. "Monday", "MO", "MO TU WE") + - type: array + items: + type: string + description: Day-of-week strings (e.g. "Monday", "MO") + byMonth: + description: > + Defines the month(s) of the year on which a recurring Event takes place. + Specified as an Integer between 1-12. January is 1. + oneOf: + - type: integer + minimum: 1 + maximum: 12 + - type: array + items: + type: integer + minimum: 1 + maximum: 12 + byMonthDay: + description: > + Defines the day(s) of the month on which a recurring Event takes place. + Specified as an Integer between 1-31. + oneOf: + - type: integer + minimum: 1 + maximum: 31 + - type: array + items: + type: integer + minimum: 1 + maximum: 31 + byMonthWeek: + description: > + Defines the week(s) of the month on which a recurring Event takes place. + Specified as an Integer between 1-5. For clarity, byMonthWeek is best + used in conjunction with byDay to indicate concepts like the first and + third Mondays of a month. + oneOf: + - type: integer + minimum: 1 + maximum: 5 + - type: array + items: + type: integer + minimum: 1 + maximum: 5 + duration: + description: > + The duration of the item (e.g., using ISO 8601 duration format). + type: string + format: duration + example: PT1H30M + endDate: + description: > + The end date and time of the item (in ISO 8601 date format). + type: string + format: date-time + example: "2025-12-31T23:59:59Z" + endTime: + description: > + The end time of the item. For a reserved event or service (e.g. EventReservation), + the time that it is expected to end. For actions that span a period of time, + when the action was performed. e.g. John wrote a book from January to December. + For media, including audio and video, it's the time offset of the end of a clip + within a larger file. + type: string + format: time + example: "18:00:00" + exceptDate: + description: > + Defines a Date or DateTime during which a scheduled Event will not take place. + The property allows exceptions to a Schedule to be specified. If an exception + is specified as a DateTime then only the event that would have started at that + specific date and time should be excluded from the schedule. If an exception is + specified as a Date then any event that is scheduled for that 24 hour period should + be excluded from the schedule. This allows a whole day to be excluded from the schedule + without having to itemise every scheduled event. + oneOf: + - type: string + format: date + - type: string + format: date-time + - type: array + items: + oneOf: + - type: string + format: date + - type: string + format: date-time + repeatCount: + description: > + Defines the number of times a recurring Event will take place. + type: integer + minimum: 1 + repeatFrequency: + description: > + Defines the frequency at which Events will occur according to a schedule Schedule. + The intervals between events should be defined as a Duration of time. + A schema:repeatFrequency value can be specified as either an ISO 8601 string + (e.g. "P1W" for weekly) or by using one of the standard values from schema.org + such as "Daily", "Weekly", "Monthly", "Yearly". + type: string + example: P1W + scheduleTimezone: + description: > + Indicates the timezone for which the time(s) indicated in the Schedule are given. + The value provided should be among those listed in the IANA Time Zone Database. + type: string + example: Asia/Kolkata + startDate: + description: > + The start date and time of the item (in ISO 8601 date format). + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + startTime: + description: > + The start time of the item. For a reserved event or service (e.g. EventReservation), + the time that it is expected to start. For actions that span a period of time, + when the action was performed. e.g. John wrote a book from January to December. + type: string + format: time + example: "09:00:00" + + Signature: + title: Beckn HTTP Signature + description: | + A digitally signed authentication credential in the HTTP `Authorization` header. + Follows draft-cavage-http-signatures-12 as profiled by BECKN-006. + + format: + ``` + Signature keyId="{subscriberId}|{uniqueKeyId}|{algorithm}", + algorithm="{algorithm}",created="{unixTimestamp}",expires="{unixTimestamp}", + headers="{signedHeaders}",signature="{base64Signature}" + ``` + + | Attribute | Description | + |-----------|-------------| + | `keyId` | `{subscriberId}|{uniqueKeyId}|{algorithm}` — FQDN, registry UUID, algorithm | + | `algorithm` | MUST be `ed25519` | + | `created` | Unix timestamp when the signature was created | + | `expires` | Unix timestamp when the signature expires | + | `headers` | Space-separated signed headers. MUST include `(created)`, `(expires)`, `digest` | + | `signature` | Base64-encoded Ed25519 signature over the signing string | + + Signing string: + `(created): {value}\n(expires): {value}\ndigest: BLAKE-512={digest}` + where `digest` is a BLAKE2b-512 hash of the request body, Base64-encoded. + type: string + pattern: '^Signature keyId="[^|"]+\|[^|"]+\|[^"]+",algorithm="[^"]+",created="\d+",expires="\d+",headers="[^"]+",signature="[A-Za-z0-9+/]+=*"$' + + + Support: + $id: https://schema.beckn.io/Support + description: Describes a support session + info + title: Support + type: object + properties: + '@context': + type: string + format: uri + default: https://schema.beckn.io/ + '@type': + type: string + const: "beckn:Support" + orderId: + description: The order against which support is required + type: string + descriptor: + description: A description of the nature of support needed + $ref: '#/components/schemas/Descriptor' + channels: + description: Available support channels described in individual linked data JSON objects + type: array + minItems: 1 + items: + $ref: '#/components/schemas/Attributes' + required: + - '@context' + - '@type' + additionalProperties: false + x-tags: + - common + + Tracking: + $id: https://schema.beckn.io/Tracking + description: Information regarding live tracking of the fufillment of a contract + title: Tracking + type: object + properties: + '@context': + description: TBD + type: string + format: uri + default: https://schema.beckn.io/ + '@type': + description: TBD + type: string + default: beckn:Tracking + refId: + description: Tracking reference ID for the tracking session. Could be the Order ID, Fulfillment ID, Fulfillment Stage ID, or any other unique tracking identifier + type: string + status: + type: string + enum: + - ACTIVE + - INACTIVE + - NOT-SUPPORTED + url: + description: Link/handle to off-network tracking UI or endpoint. + type: string + format: uri + + required: + - '@context' + - '@type' + - url + - status + x-jsonld: + '@type': schema:TrackAction + trackingStatus: $.trackingStatus + schema:target: $.url + additionalProperties: false + x-tags: + - common + + JSONLDHeaders: + type: object + properties: + '@context': + description: The JSON-LD context in which the attached object is being instantiated in. + type: string + format: uri + default: https://schema.beckn.io/ + '@type': + description: The JSON-LD type in which the attached object + type: string + default: beckn:Tracking + required: ['@context', '@type'] + + TimePeriod: + $id: "https://schema.beckn.io/TimePeriod/v2.1" + description: Time window with date-time precision for availability/validity + title: TimePeriod + x-tags: [common] + type: object + properties: + '@type': + description: JSON-LD type for a date-time period + type: string + example: TimePeriod + startDate: + description: Start instant (inclusive) + type: string + format: date-time + example: '2025-01-27T09:00:00Z' + x-jsonld: + '@id': schema:startDate + endDate: + description: End instant (exclusive or inclusive per domain semantics) + type: string + format: date-time + example: '2025-12-31T23:59:59Z' + x-jsonld: + '@id': schema:endDate + startTime: + description: Start time of the time period + type: string + format: time + example: 09:00:00 + x-jsonld: + '@id': schema:startTime + endTime: + description: End time of the time period + type: string + format: time + example: '22:00:00' + x-jsonld: + '@id': schema:endTime + required: + - '@type' + anyOf: + - required: + - startDate + - required: + - endDate + - required: + - startTime + - endTime + additionalProperties: false + + # ── Ack schema (shared by Ack, AckNoCallback, NackBadRequest, NackUnauthorized responses) ── + AckSchema: + $id: 'https://schema.beckn.io/Ack' + title: Ack + oneOf: + - type: object + - type: object + properties: + status: + description: ACK if the request was accepted; NACK if rejected. + type: string + enum: + - ACK + - NACK + signature: + description: Counter-signature proving the receiver authenticated and processed the inbound request. + $ref: '#/components/schemas/CounterSignature' + error: + description: Optional error detail explaining why no callback will follow. + $ref: '#/components/schemas/Error' + required: + - status + - signature + + responses: + Ack: + description: Synchronous receipt acknowledgement returned for every accepted Beckn request. + content: + application/json: + schema: + $ref: '#/components/schemas/AckSchema' + + AckNoCallback: + description: | + Request received but no callback will follow (e.g. agents not found, + inventory unavailable, provider closed, context.try preview complete). + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/AckSchema' + - required: + - status + - signature + - error + + + NackBadRequest: + description: 'NACK — Bad Request: Malformed or invalid request; the server could not parse or validate the payload.' + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/AckSchema' + - type: object + properties: + status: + type: string + const: NACK + error: + $ref: '#/components/schemas/Error' + + NackUnauthorized: + description: Invalid or missing authentication credentials; signature could not be verified. + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/AckSchema' + - type: object + properties: + status: + type: string + const: NACK + error: + $ref: '#/components/schemas/Error' + + ServerError: + description: Internal failure on the network participant's application; the request could not be processed. + The response body MAY contain an `error` object with additional details. + content: + application/json: + schema: + type: object + properties: + error: + $ref: '#/components/schemas/Error' + + \ No newline at end of file diff --git a/benchmarks/e2e/testdata/confirm_request.json b/benchmarks/e2e/testdata/confirm_request.json new file mode 100644 index 0000000..ff0f42c --- /dev/null +++ b/benchmarks/e2e/testdata/confirm_request.json @@ -0,0 +1,84 @@ +{ + "context": { + "action": "confirm", + "bapId": "sandbox.food-finder.com", + "bapUri": "http://bench-bap.example.com", + "bppId": "bench-bpp.example.com", + "bppUri": "BENCH_BPP_URL", + "messageId": "BENCH_MESSAGE_ID", + "transactionId": "BENCH_TRANSACTION_ID", + "timestamp": "BENCH_TIMESTAMP", + "ttl": "PT30S", + "version": "2.0.0" + }, + "message": { + "order": { + "provider": { + "id": "bench-provider-001" + }, + "items": [ + { + "id": "bench-item-001", + "quantity": { + "selected": { + "count": 1 + } + } + } + ], + "billing": { + "name": "Bench User", + "address": "123 Bench Street, Bangalore, 560001", + "city": { + "name": "Bangalore" + }, + "state": { + "name": "Karnataka" + }, + "country": { + "code": "IND" + }, + "area_code": "560001", + "email": "bench@example.com", + "phone": "9999999999" + }, + "fulfillments": [ + { + "id": "f1", + "type": "Delivery", + "stops": [ + { + "type": "end", + "location": { + "gps": "12.9716,77.5946", + "area_code": "560001" + }, + "contact": { + "phone": "9999999999", + "email": "bench@example.com" + } + } + ], + "customer": { + "person": { + "name": "Bench User" + }, + "contact": { + "phone": "9999999999", + "email": "bench@example.com" + } + } + } + ], + "payments": [ + { + "type": "ON-FULFILLMENT", + "params": { + "amount": "150.00", + "currency": "INR" + } + } + ] + } + } +} diff --git a/benchmarks/e2e/testdata/discover_request.json b/benchmarks/e2e/testdata/discover_request.json new file mode 100644 index 0000000..280ee94 --- /dev/null +++ b/benchmarks/e2e/testdata/discover_request.json @@ -0,0 +1,17 @@ +{ + "context": { + "action": "discover", + "bapId": "sandbox.food-finder.com", + "bapUri": "http://bench-bap.example.com", + "messageId": "BENCH_MESSAGE_ID", + "transactionId": "BENCH_TRANSACTION_ID", + "timestamp": "BENCH_TIMESTAMP", + "ttl": "PT30S", + "version": "2.0.0" + }, + "message": { + "intent": { + "textSearch": "pizza" + } + } +} diff --git a/benchmarks/e2e/testdata/init_request.json b/benchmarks/e2e/testdata/init_request.json new file mode 100644 index 0000000..d98f797 --- /dev/null +++ b/benchmarks/e2e/testdata/init_request.json @@ -0,0 +1,80 @@ +{ + "context": { + "action": "init", + "bapId": "sandbox.food-finder.com", + "bapUri": "http://bench-bap.example.com", + "bppId": "bench-bpp.example.com", + "bppUri": "BENCH_BPP_URL", + "messageId": "BENCH_MESSAGE_ID", + "transactionId": "BENCH_TRANSACTION_ID", + "timestamp": "BENCH_TIMESTAMP", + "ttl": "PT30S", + "version": "2.0.0" + }, + "message": { + "order": { + "provider": { + "id": "bench-provider-001" + }, + "items": [ + { + "id": "bench-item-001", + "quantity": { + "selected": { + "count": 1 + } + } + } + ], + "billing": { + "name": "Bench User", + "address": "123 Bench Street, Bangalore, 560001", + "city": { + "name": "Bangalore" + }, + "state": { + "name": "Karnataka" + }, + "country": { + "code": "IND" + }, + "area_code": "560001", + "email": "bench@example.com", + "phone": "9999999999" + }, + "fulfillments": [ + { + "id": "f1", + "type": "Delivery", + "stops": [ + { + "type": "end", + "location": { + "gps": "12.9716,77.5946", + "area_code": "560001" + }, + "contact": { + "phone": "9999999999", + "email": "bench@example.com" + } + } + ], + "customer": { + "person": { + "name": "Bench User" + }, + "contact": { + "phone": "9999999999", + "email": "bench@example.com" + } + } + } + ], + "payments": [ + { + "type": "ON-FULFILLMENT" + } + ] + } + } +} diff --git a/benchmarks/e2e/testdata/routing-BAPCaller.yaml b/benchmarks/e2e/testdata/routing-BAPCaller.yaml new file mode 100644 index 0000000..41b0617 --- /dev/null +++ b/benchmarks/e2e/testdata/routing-BAPCaller.yaml @@ -0,0 +1,13 @@ +# Routing config for v2.0.0 benchmark. Domain is not required for v2.x.x — the +# router ignores it and routes purely by version + endpoint. +# BENCH_BPP_URL is substituted at runtime with the mock BPP server URL. +routingRules: + - version: "2.0.0" + targetType: "url" + target: + url: "BENCH_BPP_URL" + endpoints: + - discover + - select + - init + - confirm diff --git a/benchmarks/e2e/testdata/select_request.json b/benchmarks/e2e/testdata/select_request.json new file mode 100644 index 0000000..23042b9 --- /dev/null +++ b/benchmarks/e2e/testdata/select_request.json @@ -0,0 +1,55 @@ +{ + "context": { + "action": "select", + "bapId": "sandbox.food-finder.com", + "bapUri": "http://bench-bap.example.com", + "bppId": "bench-bpp.example.com", + "bppUri": "BENCH_BPP_URL", + "messageId": "BENCH_MESSAGE_ID", + "transactionId": "BENCH_TRANSACTION_ID", + "timestamp": "BENCH_TIMESTAMP", + "ttl": "PT30S", + "version": "2.0.0" + }, + "message": { + "order": { + "provider": { + "id": "bench-provider-001" + }, + "items": [ + { + "id": "bench-item-001", + "quantity": { + "selected": { + "count": 1 + } + } + } + ], + "fulfillments": [ + { + "id": "f1", + "type": "Delivery", + "stops": [ + { + "type": "end", + "location": { + "gps": "12.9716,77.5946", + "area_code": "560001" + }, + "contact": { + "phone": "9999999999", + "email": "bench@example.com" + } + } + ] + } + ], + "payments": [ + { + "type": "ON-FULFILLMENT" + } + ] + } + } +} diff --git a/benchmarks/run_benchmarks.sh b/benchmarks/run_benchmarks.sh new file mode 100755 index 0000000..d4753d1 --- /dev/null +++ b/benchmarks/run_benchmarks.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# ============================================================================= +# run_benchmarks.sh — beckn-onix adapter benchmark runner +# +# Usage: +# cd beckn-onix +# bash benchmarks/run_benchmarks.sh +# +# Requirements: +# - Go 1.24+ installed +# - benchstat is declared as a tool in go.mod; invoked via "go tool benchstat" +# +# Output: +# benchmarks/results// +# run1.txt, run2.txt, run3.txt — raw go test -bench output +# parallel_cpu1.txt ... cpu16.txt — concurrency sweep +# benchstat_summary.txt — statistical aggregation +# ============================================================================= +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RESULTS_DIR="$REPO_ROOT/benchmarks/results/$(date +%Y-%m-%d_%H-%M-%S)" +BENCH_PKG="./benchmarks/e2e/..." +BENCH_TIMEOUT="10m" +# ── Smoke-test values (small): swap to the "full run" values below once stable ─ +BENCH_TIME_SERIAL="10s" # full run: 10s +BENCH_TIME_PARALLEL="30s" # full run: 30s +BENCH_COUNT=1 # full run: keep at 1; benchstat uses the 3 serial files + +cd "$REPO_ROOT" + +# ── benchstat is declared as a go tool in go.mod; no separate install needed ── +# Use: go tool benchstat (works anywhere without PATH changes) + +# bench_filter: tee full output to the .log file for debugging, and write a +# clean copy (only benchstat-parseable lines) to the .txt file. +# The adapter logger is silenced via zerolog.SetGlobalLevel(zerolog.Disabled) +# in TestMain, so stdout should already be clean; the grep is a safety net for +# any stray lines from go test itself (build output, redis warnings, etc.). +bench_filter() { + local txt="$1" log="$2" + tee "$log" | grep -E "^(Benchmark|goos:|goarch:|pkg:|cpu:|ok |PASS|FAIL|--- )" > "$txt" || true +} + +# ── Create results directory ────────────────────────────────────────────────── +mkdir -p "$RESULTS_DIR" +echo "=== beckn-onix Benchmark Runner ===" +echo "Results dir : $RESULTS_DIR" +echo "Package : $BENCH_PKG" +echo "" + +# ── Serial runs (3x for benchstat stability) ────────────────────────────────── +echo "Running serial benchmarks (3 runs × ${BENCH_TIME_SERIAL})..." +for run in 1 2 3; do + echo " Run $run/3..." + go test \ + -timeout="$BENCH_TIMEOUT" \ + -run=^$ \ + -bench="." \ + -benchtime="$BENCH_TIME_SERIAL" \ + -benchmem \ + -count="$BENCH_COUNT" \ + "$BENCH_PKG" 2>&1 | bench_filter "$RESULTS_DIR/run${run}.txt" "$RESULTS_DIR/run${run}.log" + echo " Saved → $RESULTS_DIR/run${run}.txt (full log → run${run}.log)" +done +echo "" + +# ── Concurrency sweep ───────────────────────────────────────────────────────── +echo "Running parallel concurrency sweep (cpu=1,2,4,8,16; ${BENCH_TIME_PARALLEL} each)..." +for cpu in 1 2 4 8 16; do + echo " GOMAXPROCS=$cpu..." + go test \ + -timeout="$BENCH_TIMEOUT" \ + -run=^$ \ + -bench="BenchmarkBAPCaller_Discover_Parallel|BenchmarkBAPCaller_RPS" \ + -benchtime="$BENCH_TIME_PARALLEL" \ + -benchmem \ + -cpu="$cpu" \ + -count=1 \ + "$BENCH_PKG" 2>&1 | bench_filter "$RESULTS_DIR/parallel_cpu${cpu}.txt" "$RESULTS_DIR/parallel_cpu${cpu}.log" + echo " Saved → $RESULTS_DIR/parallel_cpu${cpu}.txt (full log → parallel_cpu${cpu}.log)" +done +echo "" + +# ── Percentile benchmark ────────────────────────────────────────────────────── +echo "Running percentile benchmark (${BENCH_TIME_SERIAL})..." +go test \ + -timeout="$BENCH_TIMEOUT" \ + -run=^$ \ + -bench="BenchmarkBAPCaller_Discover_Percentiles" \ + -benchtime="$BENCH_TIME_SERIAL" \ + -benchmem \ + -count=1 \ + "$BENCH_PKG" 2>&1 | bench_filter "$RESULTS_DIR/percentiles.txt" "$RESULTS_DIR/percentiles.log" +echo " Saved → $RESULTS_DIR/percentiles.txt (full log → percentiles.log)" +echo "" + +# ── Cache comparison ────────────────────────────────────────────────────────── +echo "Running cache warm vs cold comparison..." +go test \ + -timeout="$BENCH_TIMEOUT" \ + -run=^$ \ + -bench="BenchmarkBAPCaller_Cache" \ + -benchtime="$BENCH_TIME_SERIAL" \ + -benchmem \ + -count=1 \ + "$BENCH_PKG" 2>&1 | bench_filter "$RESULTS_DIR/cache_comparison.txt" "$RESULTS_DIR/cache_comparison.log" +echo " Saved → $RESULTS_DIR/cache_comparison.txt (full log → cache_comparison.log)" +echo "" + +# ── benchstat statistical summary ───────────────────────────────────────────── +echo "Running benchstat statistical analysis..." +go tool benchstat \ + "$RESULTS_DIR/run1.txt" \ + "$RESULTS_DIR/run2.txt" \ + "$RESULTS_DIR/run3.txt" \ + > "$RESULTS_DIR/benchstat_summary.txt" 2>&1 +echo " Saved → $RESULTS_DIR/benchstat_summary.txt" +echo "" + +# ── Parse results to CSV ────────────────────────────────────────────────────── +if command -v go &>/dev/null; then + echo "Parsing results to CSV..." + go run benchmarks/tools/parse_results.go \ + -dir="$RESULTS_DIR" \ + -out="$RESULTS_DIR" 2>&1 || echo " (parse_results.go: optional step, skipping if errors)" +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── +echo "" +echo "========================================" +echo "✅ Benchmark run complete!" +echo "" +echo "Results written to:" +echo " $RESULTS_DIR" +echo "" +echo "Key files:" +echo " benchstat_summary.txt — statistical analysis of 3 serial runs" +echo " parallel_cpu*.txt — concurrency sweep results" +echo " percentiles.txt — p50/p95/p99 latency data" +echo " cache_comparison.txt — warm vs cold Redis cache comparison" +echo "" +echo "To view benchstat summary:" +echo " cat $RESULTS_DIR/benchstat_summary.txt" +echo "========================================" diff --git a/benchmarks/tools/parse_results.go b/benchmarks/tools/parse_results.go new file mode 100644 index 0000000..e792715 --- /dev/null +++ b/benchmarks/tools/parse_results.go @@ -0,0 +1,258 @@ +// parse_results.go — Parses raw go test -bench output from the benchmark results +// directory and produces two CSV files for analysis and reporting. +// +// Usage: +// +// go run benchmarks/tools/parse_results.go \ +// -dir=benchmarks/results// \ +// -out=benchmarks/results// +// +// Output files: +// +// latency_report.csv — per-benchmark mean, p50, p95, p99 latency, allocs +// throughput_report.csv — RPS at each GOMAXPROCS level from the parallel sweep +package main + +import ( + "bufio" + "encoding/csv" + "flag" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +var ( + // Matches standard go bench output: + // BenchmarkFoo-8 1000 1234567 ns/op 1234 B/op 56 allocs/op + benchLineRe = regexp.MustCompile( + `^(Benchmark\S+)\s+\d+\s+([\d.]+)\s+ns/op` + + `(?:\s+([\d.]+)\s+B/op)?` + + `(?:\s+([\d.]+)\s+allocs/op)?` + + `(?:\s+([\d.]+)\s+p50_µs)?` + + `(?:\s+([\d.]+)\s+p95_µs)?` + + `(?:\s+([\d.]+)\s+p99_µs)?` + + `(?:\s+([\d.]+)\s+req/s)?`, + ) + + // Matches custom metric lines in percentile output. + metricRe = regexp.MustCompile(`([\d.]+)\s+(p50_µs|p95_µs|p99_µs|req/s)`) +) + +type benchResult struct { + name string + nsPerOp float64 + bytesOp float64 + allocsOp float64 + p50 float64 + p95 float64 + p99 float64 + rps float64 +} + +// cpuResult pairs a GOMAXPROCS value with a benchmark result from the parallel sweep. +type cpuResult struct { + cpu int + res benchResult +} + +func main() { + dir := flag.String("dir", ".", "Directory containing benchmark result files") + out := flag.String("out", ".", "Output directory for CSV files") + flag.Parse() + + if err := os.MkdirAll(*out, 0o755); err != nil { + fmt.Fprintf(os.Stderr, "ERROR creating output dir: %v\n", err) + os.Exit(1) + } + + // ── Parse serial runs (run1.txt, run2.txt, run3.txt) ───────────────────── + var latencyResults []benchResult + for _, runFile := range []string{"run1.txt", "run2.txt", "run3.txt"} { + path := filepath.Join(*dir, runFile) + results, err := parseRunFile(path) + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: could not parse %s: %v\n", runFile, err) + continue + } + latencyResults = append(latencyResults, results...) + } + + // Also parse percentiles file for p50/p95/p99. + percPath := filepath.Join(*dir, "percentiles.txt") + if percResults, err := parseRunFile(percPath); err == nil { + latencyResults = append(latencyResults, percResults...) + } + + if err := writeLatencyCSV(filepath.Join(*out, "latency_report.csv"), latencyResults); err != nil { + fmt.Fprintf(os.Stderr, "ERROR writing latency CSV: %v\n", err) + os.Exit(1) + } + fmt.Printf("Written: %s\n", filepath.Join(*out, "latency_report.csv")) + + // ── Parse parallel sweep (parallel_cpu*.txt) ────────────────────────────── + var throughputRows []cpuResult + + for _, cpu := range []int{1, 2, 4, 8, 16} { + path := filepath.Join(*dir, fmt.Sprintf("parallel_cpu%d.txt", cpu)) + results, err := parseRunFile(path) + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: could not parse parallel_cpu%d.txt: %v\n", cpu, err) + continue + } + for _, r := range results { + throughputRows = append(throughputRows, cpuResult{cpu: cpu, res: r}) + } + } + + if err := writeThroughputCSV(filepath.Join(*out, "throughput_report.csv"), throughputRows); err != nil { + fmt.Fprintf(os.Stderr, "ERROR writing throughput CSV: %v\n", err) + os.Exit(1) + } + fmt.Printf("Written: %s\n", filepath.Join(*out, "throughput_report.csv")) +} + +// parseRunFile reads a go test -bench output file and returns all benchmark results. +func parseRunFile(path string) ([]benchResult, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + var results []benchResult + currentBench := "" + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // Main benchmark line. + if m := benchLineRe.FindStringSubmatch(line); m != nil { + r := benchResult{name: stripCPUSuffix(m[1])} + r.nsPerOp = parseFloat(m[2]) + r.bytesOp = parseFloat(m[3]) + r.allocsOp = parseFloat(m[4]) + r.p50 = parseFloat(m[5]) + r.p95 = parseFloat(m[6]) + r.p99 = parseFloat(m[7]) + r.rps = parseFloat(m[8]) + results = append(results, r) + currentBench = r.name + continue + } + + // Custom metric lines (e.g., "123.4 p50_µs"). + if currentBench != "" { + for _, mm := range metricRe.FindAllStringSubmatch(line, -1) { + val := parseFloat(mm[1]) + metric := mm[2] + for i := range results { + if results[i].name == currentBench { + switch metric { + case "p50_µs": + results[i].p50 = val + case "p95_µs": + results[i].p95 = val + case "p99_µs": + results[i].p99 = val + case "req/s": + results[i].rps = val + } + } + } + } + } + } + return results, scanner.Err() +} + +func writeLatencyCSV(path string, results []benchResult) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + w := csv.NewWriter(f) + defer w.Flush() + + header := []string{"benchmark", "mean_ms", "p50_µs", "p95_µs", "p99_µs", "allocs_op", "bytes_op"} + if err := w.Write(header); err != nil { + return err + } + + for _, r := range results { + row := []string{ + r.name, + fmtFloat(r.nsPerOp / 1e6), // ns/op → ms + fmtFloat(r.p50), + fmtFloat(r.p95), + fmtFloat(r.p99), + fmtFloat(r.allocsOp), + fmtFloat(r.bytesOp), + } + if err := w.Write(row); err != nil { + return err + } + } + return nil +} + +func writeThroughputCSV(path string, rows []cpuResult) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + w := csv.NewWriter(f) + defer w.Flush() + + header := []string{"gomaxprocs", "benchmark", "rps", "mean_latency_ms", "p95_latency_ms"} + if err := w.Write(header); err != nil { + return err + } + + for _, row := range rows { + r := []string{ + strconv.Itoa(row.cpu), + row.res.name, + fmtFloat(row.res.rps), + fmtFloat(row.res.nsPerOp / 1e6), + fmtFloat(row.res.p95), + } + if err := w.Write(r); err != nil { + return err + } + } + return nil +} + +// stripCPUSuffix removes trailing "-N" goroutine count suffixes from benchmark names. +func stripCPUSuffix(name string) string { + if idx := strings.LastIndex(name, "-"); idx > 0 { + if _, err := strconv.Atoi(name[idx+1:]); err == nil { + return name[:idx] + } + } + return name +} + +func parseFloat(s string) float64 { + if s == "" { + return 0 + } + v, _ := strconv.ParseFloat(s, 64) + return v +} + +func fmtFloat(v float64) string { + if v == 0 { + return "" + } + return strconv.FormatFloat(v, 'f', 3, 64) +}