API reference
Authoritative reference for every CATALG endpoint, request and response shape, field semantics, error codes, and shared terminology.
Authoritative reference for every CATALG endpoint, request and response shape, field semantics, error codes, and shared terminology. Use this page as a lookup. For the concepts behind these endpoints, see Catalog Concepts.
Endpoints at a glance
Flow | Caller → Receiver | Endpoint | Type |
|---|---|---|---|
Subscribe | DS → CATALG | POST /catalog/subscription | Synchronous |
List subscriptions | DS → CATALG | GET /catalog/subscription | Synchronous |
Deactivate subscription | DS → CATALG | DELETE /catalog/subscription?subscriptionId=… | Synchronous |
Publish catalog | PN → CATALG | POST /catalog/publish | ACK + async on_publish |
Publish result | CATALG → PN | POST /catalog/on_publish | Asynchronous callback |
Push catalog | CATALG → DS | POST /catalog/push | Synchronous (immediate ACK / NACK) |
Pull catalogs | DS → CATALG | POST /catalog/pull | ACK + async on_pull |
Pull result | CATALG → DS | POST /catalog/on_pull | Asynchronous callback |
Search catalogs | Caller → CATALG | POST /catalog/search | Synchronous (response action: catalog/on_search) |
Discover | CN → DS | POST /discover | Discovery Service endpoint — included here for completeness |
Discovery result | DS → CN | POST /on_discover | Asynchronous callback |
Authentication
All requests require a Beckn HTTP Signature in the Authorization header. The auth scheme name is Signature. The header is a single line — the only whitespace permitted is the single space after Signature and the spaces that separate the three tokens inside the headers="(created) (expires) digest" value. All other separators are commas with no surrounding whitespace.
Template:
Authorization: Signature keyId="{subscriberId}|{recordId}|{algorithm}",algorithm="ed25519",created="{unix_timestamp}",expires="{unix_timestamp}",headers="(created) (expires) digest",signature="{base64-ed25519-signature}"
Concrete example:
Authorization: Signature keyId="local-retail.net/grocery-net|bpp-key-001|ed25519",algorithm="ed25519",created="1748332800",expires="1748333400",headers="(created) (expires) digest",signature="BASE64SIGNATURE=="
The keyId value is composed of three pipe-separated segments:
Segment | Meaning |
|---|---|
subscriberId | The participant's identity in the DeDi registry, in the form namespace_id/registry_id — namespace_id is the network namespace (e.g. local-retail.net) and registry_id is the participant's record within it (e.g. grocery-net) |
recordId | Identifier of the specific signing-key record registered against that subscriber |
algorithm | Signing algorithm — ed25519 |
The receiving service resolves the sender's public key from the DeDi registry using subscriberId + recordId and verifies the signature against the canonical signing string. The digest header carries the BLAKE2b-512 hash of the request body, Base64-encoded and prefixed with the algorithm tag — BLAKE-512={Base64} — matching the wire pattern ^BLAKE-512=[A-Za-z0-9+/]+=*$.
The catalog object
Every catalog submitted to POST /catalog/publish follows this shape:
Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Stable catalog identifier. Updates to the same id are merged or replaced by updateMode |
descriptor | object | yes | Human / agent-readable description of this catalog |
provider | object | yes | Publishing entity. Carries id, descriptor, optional availableAt[], optional providerAttributes |
resources | array | at least one | Resources offered by the provider. At least one of resources or offers must be present |
offers | array | at least one | Commercial terms attached to resources. At least one of resources or offers must be present |
bppId | string | no | PN identifier that publishes this catalog |
bppUri | string (uri) | no | Beckn Protocol API base URL of the PN |
isActive | boolean | no | Whether the catalog is active. Defaults to true |
validity | object | no | TimePeriod (startDate / endDate) during which this catalog is valid |
At least one of resources or offers must be present. Both is common.
Resource fields
Field | Required | Description |
|---|---|---|
id | yes | Globally unique identifier of the resource |
descriptor | no | Human / agent-readable description of the resource |
resourceAttributes | no | Domain-specific fields. When present MUST carry @context (URI) and @type |
Provider-level locations are declared on provider.availableAt[] (an array of Location objects). The Resource schema itself does not carry location fields — geo-availability is inherited from the publishing provider.
Offer fields
Field | Required | Description |
|---|---|---|
id | yes | Unique identifier of the offer |
descriptor | no | Human / agent-readable description of the offer |
provider | no | Provider associated with the offer |
resourceIds | no | References (IDs) to resources covered by this offer. If omitted, the offer applies to all resources from the provider |
addOns | no | List of add-ons that may be included with this offer |
considerations | no | Proposed values to be exchanged under this offer (e.g. price) |
validity | no | TimePeriod (startDate / endDate) constraining the offer |
offerAttributes | no | Domain-specific fields. When present MUST carry @context (URI) and @type |
publishDirectives
publishDirectives is an optional array carried at the message level (not inside any catalog). Each entry, when supplied, controls how CATALG processes one catalog and is keyed by catalogId. When publishDirectives is omitted entirely, CATALG applies its own defaults to every catalog in the request.
"publishDirectives": [ { "catalogId": "CAT-FRESHMART-2026", "catalogType": "REGULAR", "updateMode": "MERGE", "visibleTo": ["local-retail.net/grocery-net"] } ]
Field | Required | Default | Description |
|---|---|---|---|
catalogId | yes | — | Must match a catalog id in message.catalogs[] of this request |
catalogType | yes (within an entry) | — | Declares the type of this catalog — MASTER (canonical resource definitions reusable across the network) or REGULAR (provider-specific catalog that can optionally extend master resources). Required only when a publishDirectives entry is supplied |
updateMode | no | MERGE | MERGE upserts resources by id — existing resources for this catalogId not in this request are retained. FULL replaces the entire catalog with this request |
visibleTo | no | nfh.global/beckn-nodes | Restricts delivery to the listed network participant identifiers (registry-registered). When omitted, the catalog is delivered to all subscribers of the default global network nfh.global/beckn-nodes |
resourceDirectives | no | — | Per-resource directives linking individual resources to master resources they inherit from. Applies only when catalogType is REGULAR |
Fields inside each resourceDirectives entry:
Field | Required | Description |
|---|---|---|
resourceId | yes | Must match a resource id in catalogs[].resources[] of this request |
extends | yes (within an entry) | Object linking this resource to a master resource it inherits from. Required whenever a resourceDirectives entry is supplied |
extends.masterResourceId | yes (within extends) | The id of the master resource to inherit from. Must refer to a resource previously published in a master catalog within the same network |
variant | no | Object that classifies this resource as a variant of another (size, colour, etc.) |
variant.type | yes (within variant) | Category that groups related variants together (any string) |
yes (within variant) | Identifier for this specific variant within the given type. The combination of type + id must be unique per resource | |
variant.label | no | Human-readable name for this variant |
Default network behaviour. When publishDirectives is omitted entirely, or when an entry has no visibleTo, the catalog is distributed via the default global network nfh.global/beckn-nodes. Every Discovery Service subscribed to that network receives the catalog. This is the catch-all path that lets a Provider Node publish without explicit visibility configuration — keeping the catalog broadly discoverable across the Fabric.
Defaults at a glance. publishDirectives itself is optional — omit it and CATALG applies its own defaults to every catalog in the request. The moment you provide an entry, catalogType is mandatory. updateMode defaults to MERGE.
POST /catalog/subscription
Create a subscription. The DS declares which networkIds and/or schemaTypes it wants to receive. The response is synchronous — a successful request returns 200 OK with a CatalogSubscriptionResponse carrying the assigned subscriptionId and status: ACTIVE. The subscription is bound to the caller's identity (derived from the Authorization header) — only that identity can later list, view, update, or deactivate it.
Request
POST /catalog/subscription Authorization: Signature keyId="local-retail.net/grocery-net|ds-key-001|ed25519",algorithm="ed25519",created="...",expires="...",headers="(created) (expires) digest",signature="..."
{ "context": { "version": "2.0.0", "action": "catalog/subscription", "messageId": "a1b2c3d4-0000-0000-0000-000000000001", "transactionId": "a1b2c3d4-0000-0000-0000-000000000002", "timestamp": "2026-05-27T08:00:00.000Z", "bapId": "ds.local-retail.net", "bapUri": "https://ds.local-retail.net" }, "message": { "subscription": { "networkIds": ["local-retail.net/grocery-net"], "schemaTypes": ["https://schema.beckn.io/RetailResource/2.1/context.jsonld"] } } }
At least one of networkIds or schemaTypes must be present. Empty schemaTypes matches all schema types on the subscribed networks.
Response (200 OK)
{ "context": { "action": "catalog/on_subscription", "version": "2.0.0", "messageId": "a1b2c3d4-0000-0000-0000-000000000001", "transactionId": "a1b2c3d4-0000-0000-0000-000000000002", "timestamp": "2026-05-27T08:00:00.000Z" }, "message": { "subscriptions": [ { "subscriptionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "status": "ACTIVE", "networkIds": ["local-retail.net/grocery-net"], "schemaTypes": ["https://schema.beckn.io/RetailResource/2.1/context.jsonld"], "createdAt": "2026-05-27T08:00:00.000Z", "updatedAt": "2026-05-27T08:00:00.000Z" } ] } }
Shared response envelope. POST /catalog/subscription, GET /catalog/subscription, and DELETE /catalog/subscription all return the same CatalogSubscriptionResponse envelope — message.subscriptions[] is always an array, even when a create or deactivate operation affects a single subscription. List operations may return one or many entries depending on filters.
List, deactivate
Operation | Request | Notes |
|---|---|---|
List | GET /catalog/subscription | Optional ?status=ACTIVE|INACTIVE, ?subscriptionId=<id>, ?limit=, ?offset=. Omitting ?status returns all statuses |
Deactivate | DELETE /catalog/subscription?subscriptionId=<id> | Marks subscription INACTIVE (no body) |
The caller MUST be the subscription owner. CATALG derives the caller's identity from the Authorization header on the request and compares it to the identity that originally created the subscription — calls from any other identity return 403 NackForbidden carrying error.code: AUT_UNAUTHORIZED_ACTION.
List response
{ "context": { "action": "catalog/on_subscription", "version": "2.0.0", "messageId": "a1b2c3d4-0000-0000-0000-000000000010", "timestamp": "2026-05-27T08:05:00.000Z" }, "message": { "subscriptions": [ { "subscriptionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "networkIds": ["local-retail.net/grocery-net"], "schemaTypes": ["https://schema.beckn.io/RetailResource/2.1/context.jsonld"], "status": "ACTIVE", "createdAt": "2026-05-27T08:00:00.000Z", "updatedAt": "2026-05-27T08:00:00.000Z" } ], "pagination": { "total": 1, "limit": 20, "offset": 0 } } }
Deactivate response
{ "context": { "action": "catalog/on_subscription", "version": "2.0.0", "messageId": "a1b2c3d4-0000-0000-0000-000000000020", "timestamp": "2026-05-27T08:30:00.000Z" }, "message": { "subscriptions": [ { "subscriptionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "networkIds": ["local-retail.net/grocery-net"], "schemaTypes": ["https://schema.beckn.io/RetailResource/2.1/context.jsonld"], "status": "INACTIVE", "createdAt": "2026-05-27T08:00:00.000Z", "updatedAt": "2026-05-27T08:30:00.000Z" } ] } }
NACK example (403 — caller does not own the subscription)
{ "message": { "status": "NACK", "messageId": "a1b2c3d4-0000-0000-0000-000000000001", "error": { "code": "AUT_UNAUTHORIZED_ACTION", "message": "Caller is not authorized to act on this subscription" } } }
Error code values are drawn from the canonical Fabric ErrorCode enum (see Errors).
POST /catalog/publish
A Provider Node submits one or more Catalog objects to CATALG for validation and indexing so that Consumer Nodes may discover them via /discover. The message MUST contain a non-empty catalogs array; each Catalog MUST include id, descriptor, and provider. A 200 Ack confirms the request was accepted for processing — per-catalog results are delivered asynchronously on the /catalog/on_publish callback. The publisher correlates the callback by context.messageId.
Payload size limit — 5 MB. The publish request body may be up to 5 MB (configurable via BODY_SIZE_LIMIT, default 5mb — raised from the earlier 2 MB so large catalogs of 500+ items publish in a single request). A request that exceeds the limit is rejected with HTTP 413 and a NACK carrying error.code: SCH_SCHEMA_VALIDATION_FAILED ("request entity too large"). Split oversized submissions across multiple publish requests (e.g. by catalog or resource batch).
Request — grocery store
POST /catalog/publish Authorization: Signature keyId="local-retail.net/grocery-net|bpp-key-001|ed25519",algorithm="ed25519",created="...",expires="...",headers="(created) (expires) digest",signature="..."
{ "context": { "version": "2.0.0", "action": "catalog/publish", "messageId": "b2c3d4e5-0000-0000-0000-000000000003", "transactionId": "b2c3d4e5-0000-0000-0000-000000000004", "timestamp": "2026-05-27T09:00:00.000Z", "bppId": "bpp.freshmart.local-retail.net", "bppUri": "https://bpp.freshmart.local-retail.net" }, "message": { "publishDirectives": [ { "catalogId": "CAT-FRESHMART-2026", "catalogType": "REGULAR", "updateMode": "MERGE", "visibleTo": ["local-retail.net/grocery-net"] } ], "catalogs": [ { "id": "CAT-FRESHMART-2026", "descriptor": { "name": "FreshMart Daily Catalog", "shortDesc": "Fresh groceries — Bangalore South" }, "provider": { "id": "freshmart-blr-001", "descriptor": { "name": "FreshMart Koramangala" }, "availableAt": [ { "geo": { "type": "Point", "coordinates": [77.6278, 12.9352] }, "address": { "streetAddress": "42 80-Feet Road, Koramangala 4th Block", "addressLocality": "Bengaluru", "addressRegion": "Karnataka", "postalCode": "560034", "addressCountry": "IN" } } ] }, "resources": [ { "id": "ITEM-BASMATI-RICE-1KG", "descriptor": { "name": "India Gate Basmati Rice", "shortDesc": "Aged basmati, 1 kg pack" }, "resourceAttributes": { "@context": "https://schema.beckn.io/RetailResource/2.1/context.jsonld", "@type": "RetailResource", "brand": "India Gate", "sku": "BSMTI-1KG-IG" } } ], "offers": [ { "id": "OFFER-BASMATI-MAY26", "descriptor": { "name": "May Staples Deal" }, "resourceIds": ["ITEM-BASMATI-RICE-1KG"], "validity": { "startDate": "2026-05-01T00:00:00Z", "endDate": "2026-05-31T23:59:59Z" }, "offerAttributes": { "@context": "https://schema.beckn.io/RetailOffer/2.1/context.jsonld", "@type": "RetailOffer", "isActive": true }, "considerations": [ { "id": "PRICE-BASMATI", "considerationAttributes": { "@context": "https://schema.beckn.io/PriceSpecification/2.0/context.jsonld", "@type": "PriceSpecification", "value": 95, "currency": "INR" } } ] } ] } ] } }
Response — ACK (200 OK)
{ "message": { "status": "ACK", "messageId": "b2c3d4e5-0000-0000-0000-000000000003" } }
Response — NACK (4xx)
{ "message": { "status": "NACK", "messageId": "b2c3d4e5-0000-0000-0000-000000000003", "error": { "code": "SCH_SCHEMA_VALIDATION_FAILED", "message": "publishDirectives[0].catalogId does not match any entry in catalogs[]" } } }
POST /catalog/on_publish
CATALG delivers one on_publish callback per /catalog/publish request — not one per catalog. After it finishes indexing all the catalogs in a publish request, CATALG sends a single callback whose results array carries one entry per catalog submitted, each with the originating catalogId and a status of ACCEPTED, REJECTED, or PARTIAL along with any per-resource errors. The publisher MUST correlate this callback with the originating publish request using context.messageId, and MUST handle REJECTED or PARTIAL results by correcting and resubmitting the affected catalogs. The Context carries a requestDigest binding this callback to the originating request.
{ "context": { "version": "2.0.0", "action": "catalog/on_publish", "messageId": "b2c3d4e5-0000-0000-0000-000000000003", "transactionId": "b2c3d4e5-0000-0000-0000-000000000004", "timestamp": "2026-05-27T09:00:02.000Z", "requestDigest": { "digest": "BLAKE-512=UWgRZ2xFP8Eaq7L0r4y5N9XGsTbm6Q8h2dXFv5z3HrBlnAa7Cw1F8gJoVtKjP9rE2mDQzhR0vXc8jL7aT5sP1Q==" } }, "message": { "results": [ { "catalogId": "CAT-FRESHMART-2026", "status": "ACCEPTED", "stats": { "itemCount": 2, "providerCount": 1, "categoryCount": 1 } } ] } }
Status | Meaning |
|---|---|
ACCEPTED | Catalog processed and distributed to matching subscribers |
REJECTED | Catalog rejected entirely — errors array explains the cause |
PARTIAL | Some resources accepted, some rejected — see errors |
When status is REJECTED or PARTIAL, each result carries an errors[] array of canonical Error objects (code, message, optional details.path):
{ "catalogId": "CAT-FRESHMART-2026", "status": "REJECTED", "errors": [ { "code": "SCH_SCHEMA_VALIDATION_FAILED", "message": "resources[0].resourceAttributes does not conform to the declared @context", "details": { "path": "$.message.catalogs[0].resources[0].resourceAttributes" } } ] }
POST /catalog/push
CATALG calls this endpoint on a Discovery Service to deliver catalog updates that match an active subscription. An active subscription matching the catalog's networkId and schema types MUST exist for the DS, and the DS MUST have a registered endpoint in the network. The body mirrors /catalog/publish — same CatalogPublishAction message schema with publishDirectives[] and catalogs[]. The DS applies MERGE or FULL per the directive.
Because CATALG initiates the push autonomously when a subscription matches — there is no prior DS request whose signature could be chained — /catalog/push uses the standard Authorization header, not the callback-chain CallbackAuthorizationHeader.
Note on bppId / bppUri. On /catalog/push, CATALG is the caller. The context.bppId and context.bppUri identify CATALG as the provider node-side sender of the push, not the Provider Node whose catalog is being delivered. Each catalog inside message.catalogs[] still carries its own bppId / bppUri identifying the originating Provider Node.
{ "context": { "version": "2.0.0", "action": "catalog/push", "messageId": "c3d4e5f6-0000-0000-0000-000000000007", "transactionId": "c3d4e5f6-0000-0000-0000-000000000008", "timestamp": "2026-05-27T09:00:02.500Z", "bppId": "catalg.example.net", "bppUri": "https://catalg.example.net" }, "message": { "publishDirectives": [ { "catalogId": "CAT-FRESHMART-2026", "catalogType": "REGULAR", "updateMode": "MERGE" } ], "catalogs": [ /* fully assembled catalogs */ ] } }
catalog/push is synchronous — the DS responds in the same HTTP response. There is no separate on_catalog_push callback.
HTTP | Body | When |
|---|---|---|
200 | Ack | Push received and accepted for processing |
202 | AckNoCallback | Push authenticated and received, but no further async processing will follow (e.g. no matching local subscription). Body MUST carry an error block describing why |
400 | NackBadRequest | Malformed body or failed schema validation |
401 | NackUnauthorized | Authorization signature missing or invalid |
429 | NackTooManyRequests | DS is rate-limiting the caller |
500 | ServerError | Unexpected failure at the DS |
Response — ACK (200)
{ "message": { "status": "ACK", "messageId": "c3d4e5f6-0000-0000-0000-000000000007" } }
Response — AckNoCallback (202)
{ "message": { "status": "ACK", "messageId": "c3d4e5f6-0000-0000-0000-000000000007", "error": { "code": "BIZ_NO_RESULTS_FOUND", "message": "No active subscription on this DS matches the pushed catalog" } } }
Response — NACK (4xx)
{ "message": { "status": "NACK", "messageId": "c3d4e5f6-0000-0000-0000-000000000007", "error": { "code": "SCH_SCHEMA_VALIDATION_FAILED", "message": "publishDirectives[0].catalogType missing" } } }
A 202 AckNoCallback is not a failure — the DS has authenticated and received the push but will not process it further (e.g. no matching local subscription). The body always carries an error block explaining why no further processing or callback will follow. Only 4xx / 5xx NACKs indicate the push itself was rejected.
POST /catalog/pull
A Discovery Service submits an asynchronous bulk catalog retrieval request to CATALG, scoped to an active subscription owned by the caller. The subscription's networkIds and schemaTypes determine which catalogs are returned — they are not specified on this request. mode selects between FULL (the latest version of each matching catalog) and INCREMENTAL (all catalog versions published between fromDate and toDate inclusive). A 200 Ack confirms the request was accepted for processing; results are delivered asynchronously on the /catalog/on_pull callback and correlated by context.messageId.
INCREMENTAL mode is not supported yet. It is defined by the spec but currently disabled. A POST /catalog/pull with mode: "INCREMENTAL" is rejected up front with an HTTP 400 NACK carrying error.code: NET_NOT_IMPLEMENTED. Use mode: "FULL" (the default). INCREMENTAL — and the fromDate / toDate bounds — will be enabled in a future release.
Request
{ "context": { "version": "2.0.0", "action": "catalog/pull", "messageId": "e1f2a3b4-0000-0000-0000-000000000099", "transactionId": "e1f2a3b4-0000-0000-0000-000000000100", "timestamp": "2026-05-27T10:00:00.000Z", "bapId": "ds.local-retail.net", "bapUri": "https://ds.local-retail.net" }, "message": { "subscriptionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "mode": "FULL", "limit": 20, "offset": 0 } }
Field | Required | Description |
|---|---|---|
subscriptionId | yes | An active subscription owned by the caller |
mode | no | FULL (default — latest state of each matching catalog). INCREMENTAL is not yet supported — see the note above; a request with mode: "INCREMENTAL" returns 400 NET_NOT_IMPLEMENTED |
fromDate | no | Inclusive lower bound for INCREMENTAL mode (ISO 8601), default 7 days before now. Ignored while INCREMENTAL is disabled. |
toDate | no | Inclusive upper bound for INCREMENTAL mode (ISO 8601), default current time. Ignored while INCREMENTAL is disabled. |
limit | no | [1, 100], default 20 |
offset | no | ≥ 0, default 0 |
Response — ACK (200 OK)
{ "message": { "status": "ACK", "messageId": "e1f2a3b4-0000-0000-0000-000000000099" } }
Response — NACK (4xx)
The synchronous NACK covers failures detectable at request time — malformed JSON, schema violations, an invalid subscriptionId (400), an unauthorized signature (401), a subscription owned by another DS (403), or a subscription that does not exist (404). Failures that surface only while assembling the result (catalog source unreadable, subscription deactivated mid-flight) are delivered on the catalog/on_pull callback with status: FAILED.
{ "message": { "status": "NACK", "messageId": "e1f2a3b4-0000-0000-0000-000000000099", "error": { "code": "CTX_INVALID_FIELD", "message": "message.subscriptionId is not a valid UUID" } } }
POST /catalog/on_pull
CATALG calls this endpoint on the Discovery Service to deliver the results of a previously accepted /catalog/pull request. The DS MUST correlate the callback with the originating request using context.messageId. When status = COMPLETED, exactly one of catalogs[] (inline) or downloadManifest (large payload, downloaded out-of-band) is present — never both. When status = FAILED, neither is present and the DS MUST read the error field instead of attempting to process any result. The Context carries a requestDigest binding the callback to the originating request.
Inline payload
{ "context": { "version": "2.0.0", "action": "catalog/on_pull", "messageId": "e1f2a3b4-0000-0000-0000-000000000099", "transactionId": "e1f2a3b4-0000-0000-0000-000000000100", "timestamp": "2026-05-27T10:01:05.000Z", "requestDigest": { "digest": "BLAKE-512=Tg5Hf9PuQ3kRn7M0XaZsBd2YcVwLpKjEi8oRmTbU6xN4qJvWy1HrCs7tFkAaEgD8vNcBqLpZmRsT3xUwYpKjAg==" } }, "message": { "status": "COMPLETED", "catalogs": [ /* fully assembled catalog objects */ ], "pagination": { "total": 12, "limit": 20, "offset": 0 } } }
Download manifest (large payloads)
{ "context": { "version": "2.0.0", "action": "catalog/on_pull", "messageId": "e1f2a3b4-0000-0000-0000-000000000099", "transactionId": "e1f2a3b4-0000-0000-0000-000000000100", "timestamp": "2026-05-27T10:01:05.000Z", "requestDigest": { "digest": "BLAKE-512=Tg5Hf9PuQ3kRn7M0XaZsBd2YcVwLpKjEi8oRmTbU6xN4qJvWy1HrCs7tFkAaEgD8vNcBqLpZmRsT3xUwYpKjAg==" } }, "message": { "status": "COMPLETED", "downloadManifest": { "url": "https://storage.example.com/catalog-pull-results/e1f2a3b4.json.gz?token=...", "expiresAt": "2026-05-27T11:01:05.000Z", "format": "json.gz", "sizeBytes": 4823104, "checksum": "sha256:a3f1e9b7c2d04..." }, "pagination": { "total": 1450, "limit": 20, "offset": 0 } } }
The DS MUST download the object at url, verify the sha256:-prefixed checksum against the downloaded content before processing, and MUST NOT process the content if verification fails. The DS MUST NOT attempt the download after expiresAt; if expired, the DS MUST re-issue POST /catalog/pull. Payloads with format: json.gz MUST be decompressed before parsing.
FAILED callback
When CATALG could not assemble the pull result — for example, the catalog source was unreadable or the subscription was deactivated mid-flight — it delivers a status: FAILED callback with an error block. The DS MUST read the error field and MUST NOT attempt to process catalogs[] or downloadManifest. The code is a value from the canonical Fabric ErrorCode enum.
{ "context": { "version": "2.0.0", "action": "catalog/on_pull", "messageId": "e1f2a3b4-0000-0000-0000-000000000099", "transactionId": "e1f2a3b4-0000-0000-0000-000000000100", "timestamp": "2026-05-27T10:01:05.000Z", "requestDigest": { "digest": "BLAKE-512=Tg5Hf9PuQ3kRn7M0XaZsBd2YcVwLpKjEi8oRmTbU6xN4qJvWy1HrCs7tFkAaEgD8vNcBqLpZmRsT3xUwYpKjAg==" } }, "message": { "status": "FAILED", "error": { "code": "NET_CATALOG_SOURCE_UNAVAILABLE", "message": "Catalog source could not be read while assembling the pull result" } } }
POST /catalog/search
A caller queries CATALG for indexed catalogs, filtered by catalog type, network identifiers, and schema-type URIs, with paginated results. All filters are optional: omitting filters.type defaults to MASTER; omitting or leaving filters.networkIds empty matches all networks; omitting or leaving filters.schemaTypes empty matches all schema types. limit MUST be between 1 and 100 (default 20); offset MUST be non-negative (default 0). The response is synchronous and is wrapped in a Fabric envelope with context.action set to catalog/on_search.
Request
{ "context": { "version": "2.0.0", "action": "catalog/search", "messageId": "...", "transactionId": "..." }, "message": { "filters": { "type": "MASTER", "networkIds": ["local-retail.net/grocery-net"], "schemaTypes": ["https://schema.beckn.io/RetailResource/2.1/context.jsonld"] }, "limit": 20, "offset": 0 } }
Field | Default | Description |
|---|---|---|
filters.type | MASTER | MASTER or REGULAR |
filters.networkIds | all | Restrict to these networks |
filters.schemaTypes | all | Restrict to these JSON-LD @context URIs |
limit / offset | 20 / 0 | Pagination, limit ∈ [1, 100] |
Response
{ "context": { "version": "2.0.0", "action": "catalog/on_search", "messageId": "d4e5f6a7-0000-0000-0000-000000000050", "transactionId": "d4e5f6a7-0000-0000-0000-000000000051", "timestamp": "2026-05-27T10:05:00.000Z" }, "message": { "catalogs": [ { "id": "CAT-...", "descriptor": { "name": "...", "shortDesc": "..." }, "provider": { "id": "...", "descriptor": { "name": "..." } }, "resources": [ /* assembled resource objects */ ] } ], "pagination": { "total": 12, "limit": 20, "offset": 0 } } }
POST /discover and POST /on_discover
Discovery is a Discovery Service endpoint, not a CATALG endpoint. It is included here so the full network flow is visible from one reference page.
The Consumer Node sends a discovery request to the DS, which evaluates the supplied intent against its indexed catalog store and returns matching results asynchronously via /on_discover. The DS keeps its index synchronized by subscribing to CATALG — the CN never interacts with CATALG directly for discovery.
Intent mechanisms overview
Mechanism | Field | Description |
|---|---|---|
Text search | intent.textSearch | Free-text or natural-language query — keywords, phrases |
JSONPath filter | intent.filters | Structured attribute filter using RFC 9535 JSONPath expressions |
Spatial filter | intent.spatial[] | Geometry operators — proximity radius, polygon, shape relations |
Any combination of these may appear in a single intent. Each mechanism is described and illustrated separately below.
Text search
intent.textSearch accepts a free-text string — keywords or a natural-language phrase. The DS queries its full-text index and returns resources whose name, description, or indexed attributes match the query.
{ "context": { "version": "2.0.0", "action": "discover", "messageId": "a1b2c3d4-0000-0000-0000-000000000001", "transactionId": "a1b2c3d4-0000-0000-0000-000000000002", "timestamp": "2026-05-27T10:10:00.000Z", "bapId": "cn.myapp.local-retail.net", "bapUri": "https://cn.myapp.local-retail.net" }, "message": { "intent": { "textSearch": "fresh vegetables" } } }
Field | Type | Description |
|---|---|---|
intent.textSearch | string | Keywords or natural-language phrase. No operators or syntax — plain text only. |
JSONPath filter
intent.filters applies a structured attribute filter using an RFC 9535 JSONPath expression. Only resources that satisfy the expression are returned.
JSONPath expressions MUST be absolute paths rooted at $, where $ is the message payload root. Resources are addressed as $.catalogs[*].resources[*] and offers as $.catalogs[*].offers[*].
{ "context": { "version": "2.0.0", "action": "discover", "messageId": "b2c3d4e5-0000-0000-0000-000000000003", "transactionId": "b2c3d4e5-0000-0000-0000-000000000004", "timestamp": "2026-05-27T10:10:00.000Z", "bapId": "cn.myapp.local-retail.net", "bapUri": "https://cn.myapp.local-retail.net" }, "message": { "intent": { "filters": { "type": "jsonpath", "expression": "$.catalogs[*].resources[*] ? (@.resourceAttributes.isActive == true)" } } } }
Field | Type | Description |
|---|---|---|
intent.filters.type | string | Always "jsonpath" |
intent.filters.expression | string | RFC 9535 filter expression. MUST be an absolute path from $. Use @ for the current node inside ? (...). |
Path examples:
Target | Expression |
|---|---|
Active resources only | $.catalogs[*].resources[*] ? (@.resourceAttributes.isActive == true) |
Resources with a specific brand | $.catalogs[*].resources[*] ? (@.resourceAttributes.brand == "India Gate") |
Offers with discount type FLAT | $.catalogs[*].offers[*] ? (@.offerAttributes.discountType == "FLAT") |
Spatial filter
intent.spatial[] is an array of spatial constraints. Each constraint specifies a geometry operation, a target field path, and a GeoJSON geometry. Only resources whose indexed location satisfies all constraints are returned.
{ "context": { "version": "2.0.0", "action": "discover", "messageId": "c3d4e5f6-0000-0000-0000-000000000005", "transactionId": "c3d4e5f6-0000-0000-0000-000000000006", "timestamp": "2026-05-27T10:10:00.000Z", "bapId": "cn.myapp.local-retail.net", "bapUri": "https://cn.myapp.local-retail.net" }, "message": { "intent": { "spatial": [ { "op": "S_DWITHIN", "targets": "$.catalogs[*].provider.availableAt[*].geo", "geometry": { "type": "Point", "coordinates": [77.6278, 12.9352] }, "distanceMeters": 3000 } ] } } }
Field | Type | Description |
|---|---|---|
spatial[].operation | string | Spatial operation to apply — see table below |
spatial[].targets | string | Absolute JSONPath to the geo field being tested. MUST match exactly how the field was indexed. For provider-level coordinates use $.catalogs[*].provider.availableAt[*].geo. |
spatial[].geometry | GeoJSON | The reference geometry — Point, Polygon, MultiPolygon, etc. Coordinates are [longitude, latitude]. |
spatial[].distanceMeters | number | Required for s_dwithin only — radius in metres from the reference point. |
targets must be an absolute JSONPath. The DS matches the targets value exactly against the path stored during indexing. Using a wrong root (e.g. $['availableAt'][*]['geo']) or the wrong level (e.g. $.catalogs[*].resources[*].availableAt[*].geo when availableAt is on provider) returns zero spatial results with no error.
Results are delivered asynchronously to the CN's bapUri at POST /on_discover, correlated by messageId.
POST /on_discover callback
The DS calls this endpoint on the CN to deliver Catalog objects matching the original intent. The message MUST carry a non-empty catalogs array. The CN MUST use context.messageId to correlate this callback with the originating /discover request. The Context carries a requestDigest binding the callback to that request.
{ "context": { "version": "2.0.0", "action": "on_discover", "messageId": "e5f6a7b8-0000-0000-0000-000000000009", "transactionId": "e5f6a7b8-0000-0000-0000-000000000010", "timestamp": "2026-05-27T10:10:01.800Z", "bppId": "ds.local-retail.net", "bppUri": "https://ds.local-retail.net", "requestDigest": { "digest": "BLAKE-512=Pn4XmJ2sQyR8tBv6Wz3LcK1NfDgEoIuVaH5bMyT0xZpUjC9rEqAhWk7yLeRsTd3vNcBqLpZmRsT3xUwYpKjAg==" } }, "message": { "catalogs": [ { "id": "CAT-FRESHMART-2026", "descriptor": { "name": "FreshMart Daily Catalog" }, "provider": { "id": "freshmart-blr-001", "descriptor": { "name": "FreshMart Koramangala" } }, "resources": [ /* matching resource objects */ ], "offers": [ /* offers that apply */ ] } ] } }
The CN correlates the callback to its original /discover request by messageId.
Errors
All NACK responses, AckNoCallback responses, and asynchronous error callbacks share the canonical Fabric Error shape:
{ "message": { "status": "NACK", "messageId": "...", "error": { "code": "SCH_FIELD_NOT_ALLOWED", "message": "Human-readable description of the failure", "details": { "path": "$.message.catalogs[0].resources[0].resourceAttributes" } } } }
error.code MUST be a value from the canonical Fabric ErrorCode enum. The prefix indicates the protocol stack layer where the error originated:
Prefix | Layer |
|---|---|
CTX_ | Context / routing |
AUT_ | Authentication / trust |
SCH_ | Core / linked-data schema |
NET_ | Networking / runtime |
BIZ_ | Application / business logic |
POL_ | Policy |
Common codes by HTTP status
HTTP | Code | Used when |
|---|---|---|
400 | SCH_INVALID_JSON | Request body is not valid JSON |
400 | SCH_REQUIRED_FIELD_MISSING | A required field is missing |
400 | SCH_SCHEMA_VALIDATION_FAILED | Body is JSON but does not conform to the endpoint schema |
413 | SCH_SCHEMA_VALIDATION_FAILED | Request body exceeds the 5 MB payload limit (BODY_SIZE_LIMIT) |
400 | SCH_FIELD_NOT_ALLOWED | Body contains an unrecognised field |
400 | SCH_INVALID_ENUM | A field value is not in the allowed enum |
400 | SCH_INVALID_FORMAT | A field value does not match its declared format |
400 | CTX_MISSING_FIELD | A required context field is absent |
400 | CTX_INVALID_FIELD | A context field has an invalid value |
400 | CTX_ACTION_MISMATCH | context.action does not match the endpoint being called |
400 | CTX_VERSION_UNSUPPORTED | context.version is not supported |
400 | CTX_TTL_EXPIRED | timestamp + ttl has elapsed before processing |
401 | AUT_SIGNATURE_MISSING | Authorization header is absent |
401 | AUT_SIGNATURE_INVALID | Signature did not verify against the resolved public key |
401 | AUT_KEY_NOT_FOUND | keyId could not be resolved in the registry |
401 | AUT_KEY_EXPIRED_OR_REVOKED | Resolved key has expired or been revoked |
401 | AUT_SUBSCRIBER_NOT_FOUND | Subscriber identity is not registered |
403 | AUT_UNAUTHORIZED_ACTION | Caller is authenticated but not permitted to perform this action on this resource |
403 | AUT_DOMAIN_NOT_ALLOWED | Caller's domain is not permitted for the requested action |
429 | AUT_RATE_LIMITED | Caller has exceeded the request rate limit |
400 | NET_NOT_IMPLEMENTED | A spec-defined capability is not yet enabled — e.g. mode: INCREMENTAL on /catalog/pull |
500 | NET_INTERNAL_ERROR | Unexpected server failure |
503 | NET_DOWNSTREAM_UNAVAILABLE | A downstream service required to process the request is unavailable |
503 | NET_OVERLOADED | Service is overloaded — retry after backoff |
Asynchronous (callback) errors
Errors discovered only after the synchronous ACK — i.e. during async processing — are reported on the relevant callback. Subscription-level failures detectable at request time (subscriptionId unknown, ownership mismatch, missing/invalid signature) are returned synchronously on /catalog/pull itself, not on the callback. The same Error shape applies.
Reported on | Code | When |
|---|---|---|
catalog/on_publish (per results[] entry) | SCH_SCHEMA_VALIDATION_FAILED | Resource attributes do not conform to the declared @context |
catalog/on_publish (per results[] entry) | BIZ_GENERIC_ERROR | Business-rule rejection that does not fit a more specific code |
catalog/on_pull | NET_CATALOG_SOURCE_UNAVAILABLE | Catalog source could not be read while assembling the pull result |
catalog/on_pull | NET_CATALOG_STALE | Available catalog data is older than the requested time window |
See the full canonical ErrorCode enum in the Beckn Protocol v2.0 specification.
Glossary
Term | Meaning |
|---|---|
CATALG | Fabric's catalog publication and distribution service — the source of truth for what is published |
DISCOVR | Fabric's discovery service — searches catalogs CATALG has distributed |
Provider Node (PN) | A network participant that publishes catalogs to CATALG |
Discovery Service (DS) | A network participant that subscribes to CATALG and serves discovery queries |
Consumer Node (CN) | A consumer-facing application that calls /discover on a DS |
Network Facilitator | The participant that creates a network and publishes master (template) catalogs |
networkId | DeDi-registered subnet identifier in the form namespace_id/registry_id. Used in publishDirectives[].visibleTo to scope distribution and in subscription networkIds[] to scope what a DS receives. Defaults to the global network nfh.global/beckn-nodes when visibleTo is omitted |
keyId | Beckn HTTP Signature key identifier in the form {subscriberId}|{recordId}|{algorithm} |
subscriberId | First segment of keyId — the participant identity registered in the DeDi registry |
recordId | Second segment of keyId — identifies the specific signing-key record within the participant's registration |
Master catalog | Network-wide template, published by the Network Facilitator. No offers |
Regular catalog | Commercial catalog published by a Provider Node |
MERGE / FULL | Update modes — incremental upsert vs. complete replacement of a catalog's resource set |