# Durable Endpoints&#x20;

Durable Endpoints let you build or transform your API into fault-tolerant endpoints simply by wrapping your critical logic into [durable steps](/docs-markdown/learn/inngest-steps).

Durable Endpoints behave like normal endpoints. The mental model stays the same: request, response. But each step brings you tracing, observability, and retry logic from the point of failure.

> **Callout:** Durable Endpoints are available in the TypeScript and Go SDKs.

## When to use Durable Endpoints

**You have endpoints that fail partway through.** Any endpoint with multiple steps where failure at step 3 means steps 1 and 2 were wasted work. Instead of writing try/catch logic everywhere or hoping for the best, simply wrap your code in steps and let failures resume from where they left off.

**You want observability without the setup.** If you want visibility into your endpoints without configuring a bunch of external services, Durable Endpoints give you that instantly.

**You're already using Inngest.** You can add durability to other endpoints without refactoring everything into a workflow or thinking heavily about event logic.

## Quick Start

If you have a traditional endpoint:

```typescript {{ title: "Next.js" }}
import { NextRequest } from "next/server";

export const POST = async (req: NextRequest) => {
  const { userId, data } = await req.json();
  
  const user = await db.users.find(userId);
  const enriched = { ...data, account: user.accountId };

  const result = await processData(enriched);

  await sendNotification(userId, result);

  return Response.json({ success: true, result });
};
```

```typescript {{ title: "Bun" }}
Bun.serve({
  port: 3000,
  routes: {
    "/process": async (req) => {
      const { userId, data } = await req.json();

      const user = await db.users.find(userId);
      enriched = { ...data, account: user.accountId };

      const result = await processData(enriched);

      await sendNotification(userId, result);

      return new Response(JSON.stringify({ success: true, result }));
    },
  },
});
```

Create an Inngest client:

```typescript {{ title: "Next.js" }}
import { Inngest } from "inngest";
import { endpointAdapter } from "inngest/next";

const inngest = new Inngest({
  id: "my-app",
  endpointAdapter,
});
```

```typescript {{ title: "Bun" }}
import { Inngest } from "inngest";
import { endpointAdapter } from "inngest/edge";

const inngest = new Inngest({
  id: "my-app",
  endpointAdapter,
});
```

Then, wrap your API endpoint with `inngest.endpoint` and move your endpoint's critical logic into `step.run` blocks:

```typescript {{ title: "Next.js" }}
import { step } from "inngest";
import { inngest } from "@/inngest/client";
import { NextRequest } from "next/server";

export const POST = inngest.endpoint(async (req: NextRequest) => {
  const { userId, data } = await req.json();

  // Step 1: Validate and enrich the data
  const enriched = await step.run("enrich-data", async () => {
    const user = await db.users.find(userId);
    return { ...data, account: user.accountId };
  });

  // Step 2: Process the enriched data
  const result = await step.run("process", async () => {
    return await processData(enriched);
  });

  // Step 3: Send notification
  await step.run("notify", async () => {
    await sendNotification(userId, result);
  });

  return Response.json({ success: true, result });
});
```

```typescript {{ title: "Bun" }}
import { Inngest, step } from "inngest";
import { endpointAdapter } from "inngest/edge";

const inngest = new Inngest({ id: "my-app", endpointAdapter });

Bun.serve({
  port: 3000,
  routes: {
    "/process": inngest.endpoint(async (req) => {
      const { userId, data } = await req.json();

      // Step 1: Validate and enrich the data
      const enriched = await step.run("enrich-data", async () => {
        const user = await db.users.find(userId);
        return { ...data, account: user.accountId };
      });

      // Step 2: Process the enriched data
      const result = await step.run("process", async () => {
        return await processData(enriched);
      });

      // Step 3: Send notification
      await step.run("notify", async () => {
        await sendNotification(userId, result);
      });

      return new Response(JSON.stringify({ success: true, result }));
    }),
  },
});
```

If `process` fails, the endpoint will retry from that step. `enrich-data` won't re-run.

**Read the [Durable Endpoint TypeScript SDK Reference](/docs-markdown/reference/typescript/durable-endpoints) for more detailed usage information.**

### Using Steps

Durable Endpoints support all the same step methods as Inngest functions. See the [Steps documentation](/docs-markdown/learn/inngest-steps) for the full reference:

- [`step.run()`](/docs-markdown/learn/inngest-steps#step-run): Reliably execute the provided block by retrying upon failure
- [`step.sleep()`](/docs-markdown/learn/inngest-steps#step-sleep): Pause execution for a duration
- [`step.waitForEvent()`](/docs-markdown/learn/inngest-steps#step-wait-for-event-step-wait-for-event): Wait for an external event

In order to start using steps within your API endpoints, you must first set up middleware to intercept HTTP requests.

```go
import (
	"context"

	"github.com/inngest/inngestgo/step"
	"github.com/inngest/inngestgo/stephttp"
)

func setuphttp() {
	// provider adds inngest support to http handlers
	provider := stephttp.Setup(stephttp.SetupOpts{
		Domain: "api.example.com", // add your api domain here.
	})

  // provider allows you to wrap individual http handlers via `provider.servehttp`,
  // and provides stdlib-compatible middleware via `provider.middleware`
  http.HandleFunc("/users", provider.ServeHTTP(handleUsers))

  // or, via middleware with, for example, chi:
  r := chi.NewRouter()
  r.Use(provider.Middleware)
  r.Get("/users", handleUsers)
}
```

Once you've added the middleware, you can configure functions and execute steps within REST endpoints directly:

```go
import (
	"context"

	"github.com/inngest/inngestgo/step"
	"github.com/inngest/inngestgo/stephttp"
)

func handleUsers(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	stephttp.Configure(ctx, stephttp.FnOpts{
		// Configure the function ID, removing IDs from the URL:
		ID: "/users/{id}"
	})

	// Step 1: Authenticate (with full observability)
	auth, err := step.Run(ctx, "authenticate", func(ctx context.Context) (*AuthResult, error) {
		// You can chain steps as usual...
		return nil, nil
	})
	if err != nil {
		http.Error(w, "Authentication failed", http.StatusUnauthorized)
		return
	}
	// ...
}
```

> **Callout:** Durable Endpoints are not yet available in the Python SDK.

## Requesting a Durable Endpoint

Durable Endpoints behave like regular API endpoints on the success path. You can request them from your front-end (*or back-end*) using `fetch()` or your favorite query or http library:

However, when a failure triggers retries, a Durable Endpoint returns a redirect to a dedicated endpoint on Inngest Cloud to poll the final result.

Here is a snippet handling both the direct result and the redirected result after retries:

```typescript
function handleError(error) {
  // ...
}

async function handleResult(result) {
  const result = await res.json()
  // ...
}

fetch(`/api/your-durable-endpoint`)
  .then((res) => {
    if (res.redirected) {
      // follow the redirect
      fetch(res.url)
        .then(handleResult)
        .catch(handleError);
    } else {
      handleResult(res)
    }
  })
  .catch(handleError);
```

> **Callout:** As the Durable Endpoint redirects the request to a dedicated endpoint on Inngest's Cloud, fetch() cannot simply follow this redirect for you (CORS policy).
> Instead, you need to get the redirect URL (res.url) and trigger a new fetch() request.

## SDK Support

| SDK        | Support | Version                         |
| ---------- | ------- | ------------------------------- |
| TypeScript | ✅ Beta  | >= 3.x (with `endpointAdapter`) |
| Go         | ✅       | >= v0.14.0                      |

## Streaming&#x20;

Durable Endpoints can stream data back to clients in real-time using Server-Sent Events (SSE). Stream LLM tokens, progress updates, or any other data while keeping full durability guarantees. If a step fails and retries, streamed data from that step is automatically rolled back on the client.

Read the [full guide](/docs-markdown/learn/durable-endpoints/streaming?ref=docs-durable-endpoints) for setup, client integration, rollback semantics, and more.

## Limitations

Durable Endpoints is currently in beta. The following limitations apply:

- **Flow control is not supported** — Features like concurrency limits and rate limiting are not available for Durable Endpoints
- **POST body is not yet supported** — Prefer using query strings for passing data. POST body support is coming soon
- **Standard HTTP responses only** — Durable Endpoints should return a standard HTTP response. Streaming responses [are supported](#streaming)

## Examples

The [Durable Endpoints example page](/docs-markdown/examples/durable-endpoints) provides practical pattern examples such as parallel steps.

The following demos are also available to check out and run locally with the Inngest Dev Server:

**"Explore the full Trip Booker example"**: [Clone this example locally to run it and explore the full source code.](https://github.com/inngest/inngest-js/tree/main/examples/durable-endpoints-trip-booker#readme)

**"Explore the full DeepResearch demo"**: [Explore a more advanced example with a DeepResearch interface entirely built with Durable Endpoints.](https://github.com/inngest/inngest-js/tree/main/examples/durable-endpoints-deepresearch#readme)

## Further Reference

- [Durable Endpoint - TypeScript SDK Reference](/docs-markdown/reference/typescript/durable-endpoints)
- [Steps Overview](/docs-markdown/learn/inngest-steps)