Skip to main content

REST and gRPC APIs

Rag.NET.Api and Rag.NET.Api.Grpc serve a pipeline over the network. Each has a matching client package — Rag.NET.Api.Client and Rag.NET.Api.Grpc.Client — that implements IRagPipeline against it, so calling code does not know whether the pipeline is in-process or three hops away.

That symmetry is the point. A service that takes IRagPipeline in its constructor works unchanged whether you register the real pipeline, the HTTP client, or the gRPC client.

When you need one​

You do not need either for a single application that ingests and queries its own corpus — host the pipeline in-process and skip this page. Reach for a server when:

  • Several applications share one corpus. One pipeline owns the vector store and the embedding spend; everything else talks to it.
  • The caller is not .NET. REST and gRPC are the two ways out of the ecosystem.
  • Ingestion and query have different scaling shapes. Ingestion is bursty and slow, query is steady and fast; separating them lets you size them separately.

If the caller is an LLM agent rather than an application, you want the MCP server instead — it exposes the same pipeline as tools rather than as endpoints.

REST​

dotnet add package Rag.NET.Api
builder.Services.AddRagNet(rag => rag
.UseQdrant("localhost", 6334, "docs", vectorDimensions: 1536));

builder.Services.AddRagNetApi(o => o.ApiKeys = [builder.Configuration["RagApiKey"]!]);

var app = builder.Build();

app.UseRagNetApiAuthentication(); // must come before MapRagNetApi
app.MapRagNetApi();

app.Run();

Endpoints​

MethodPathMaps to
POST/rag/ingestIngestAsync
POST/rag/retrieveRetrieveAsync
POST/rag/askAskAsync
GET/rag/ask/stream?query=...AskStreamingAsync, as server-sent events
DELETE/rag/documents/{documentId}DeleteAsync

RagApiOptions.RoutePrefix changes the /rag prefix.

Authentication is a decision you have to make​

AddRagNetApi throws if you have not made it. An empty ApiKeys with AllowAnonymous left false is the shape that serves every endpoint — including ingest and delete — to anyone who finds the port, so it is rejected at startup rather than at the first unwanted request:

// Throws: neither a key nor an explicit opt-out.
builder.Services.AddRagNetApi();

// Throws: the two contradict each other.
builder.Services.AddRagNetApi(o => { o.ApiKeys = ["k"]; o.AllowAnonymous = true; });

// Fine — keys required.
builder.Services.AddRagNetApi(o => o.ApiKeys = ["k1", "k2"]);

// Fine — anonymous, and you said so.
builder.Services.AddRagNetApi(o => o.AllowAnonymous = true);

Configuring keys is not enough on its own. UseRagNetApiAuthentication() is what puts the middleware in the pipeline, and omitting it used to leave the keys configured, unread, and every endpoint open. MapRagNetApi now throws when the Use call has not run, which is also why the Use call must come first: an IEndpointRouteBuilder cannot inspect the middleware pipeline, so the check is on the marker that Use sets.

Webhooks​

MapRagNetWebhooks() adds a signed endpoint that parses an inbound payload with the registered IWebhookPayloadParser and enqueues the resulting jobs on the IIngestionJobQueue — the push half of event-driven ingestion, where a SaaS source tells you a document changed instead of being polled.

builder.Services.AddRagNetWebhooks(o => o.Secret = builder.Configuration["WebhookSecret"]!);
app.MapRagNetWebhooks();

The route is deliberately exempt from API-key auth: the HMAC signature over the raw body is the credential, and the sender has no key to present. It answers 202 Accepted with { enqueued: n }, 401 on a missing or invalid signature, 400 on invalid JSON or a rejected payload, and 503 when no IIngestionJobQueue is registered.

The HTTP client​

dotnet add package Rag.NET.Api.Client
services.AddRagNetApiClient(o =>
{
o.BaseUrl = "https://rag.internal:5001";
o.ApiKey = configuration["RagApiKey"]!;
});

This registers an IRagPipeline. Inject it exactly as you would the in-process one.

gRPC​

dotnet add package Rag.NET.Api.Grpc
builder.Services.AddRagNet(rag => rag.UseQdrant(/* … */));
builder.Services.AddRagNetGrpcApi(o => o.ApiKeys = [builder.Configuration["RagApiKey"]!]);

var app = builder.Build();
app.MapRagNetGrpcApi();
app.Run();

AddRagNetGrpcApi enforces the same either-keys-or-explicit-anonymous rule as AddRagNetApi, for the same reason. MapRagNetGrpcApi() is the mapping call — the service implementation itself is internal, so there is no MapGrpcService<T>() for you to write.

The service contract is five methods, one per pipeline operation:

service RagService {
rpc Ingest (IngestRequest) returns (IngestResponse);
rpc Retrieve (RetrieveRequest) returns (RetrieveResponse);
rpc Ask (AskRequest) returns (AskResponse);
rpc AskStream (AskRequest) returns (stream AskStreamUpdate);
rpc Delete (DeleteRequest) returns (DeleteResponse);
}

rag.proto lives at src/Rag.NET.Api.Grpc/Protos/rag.proto in the repository. It is compiled into the package rather than shipped alongside it, so a non-.NET caller generating its own client takes the file from source control.

dotnet add package Rag.NET.Api.Grpc.Client
services.AddRagNetGrpcClient(o =>
{
o.BaseUrl = "https://rag.internal:5002";
o.ApiKey = configuration["RagApiKey"]!;
});

The key travels as call credentials on every request.

Choosing between them​

RESTgRPC
Non-.NET callersAny HTTP clientNeeds a generated client from rag.proto
Streaming answersServer-sent events over GETA native server-streaming call
PayloadJSONProtobuf — smaller and faster to parse
Debuggingcurl, browser dev toolsgrpcurl, or a proto-aware proxy
Metadata fidelityTyped JSONmap<string,string> — typed chunk metadata is flattened to strings on the wire

REST unless you have a reason. The metadata row is the one that bites: a filter that depends on a numeric or date-typed metadata value is comparing strings by the time it crosses gRPC.

What neither gives you​

Both expose the pipeline, not the corpus. There is no endpoint that lists documents, reports index size or replays a query's stages — that is the diagnostics package and its own opt-in endpoint. And neither carries security with it: an API key says the caller is allowed to reach the pipeline, not which chunks they are allowed to see. If chunks are access-controlled, Rag.NET.Security's RBAC and the ICallerContext binding in Rag.NET.Security.AspNetCore are what enforce that, and they are a separate decision from this one.