# ENS Omnigraph API (GraphQL)

The Omnigraph is **a GraphQL API** following the [Relay specification](https://relay.dev/graphql/connections.htm). There's no proprietary protocol or transport — any GraphQL client in any language works, from `curl` and `fetch` to [`urql`](https://nearform.com/open-source/urql/), [`Apollo`](https://www.apollographql.com/), [`graphql-request`](https://github.com/jasonkuhrt/graphql-request), and beyond.

This guide walks you through the minimum: a single `fetch` call against `/api/omnigraph` — the same flow as our [omnigraph-graphql-example](https://github.com/namehash/ensnode/tree/main/examples/omnigraph-graphql-example).

If you want end-to-end typed queries (via [`gql.tada`](https://gql-tada.0no.co/)) with editor autocomplete and a built-in client, use [`enssdk`](/docs/integrate/integration-options/enssdk) instead — but if you need to integrate from a language without first-class GraphQL tooling, or you're already in a stack with its own GraphQL client, this is the path.

<IntegrateHostedEnsNodeTip />

## 1. The endpoint

The Omnigraph lives at:

```
POST {ENSNODE_URL}/api/omnigraph
Content-Type: application/json

{ "query": "...", "variables": { ... } }
```

It returns `{ "data": ..., "errors": [...] }` per the standard GraphQL response shape.

A minimum-viable hello world over `curl`:

```sh
curl -sS -X POST \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ domain(by: { name: \"eth\" }) { canonical { name { beautified } } owner { address } } }"}' \
  https://api.v2-sepolia.ensnode.io/api/omnigraph
```
**BeautifiedName:** `beautified` is the display-ready form of the Canonical Name — its normalized labels rendered per [ENSIP-15](https://docs.ens.domains/ensip/15) (e.g. `♾.eth` → `♾️.eth`) — so you can render it directly with no normalization or emoji logic of your own. It's **display-only**: use `interpreted` (or the Domain `id`) as a lookup key or navigation target, never `beautified`. See [Beautified Name](/docs/reference/terminology#beautified-name).

The rest of this guide builds the same thing in TypeScript using `fetch`, so you have something to extend.

## 2. Scaffold a TypeScript project

If you already have one, skip ahead to [Write the query](#3-write-the-query).

```sh
mkdir my-ens-script && cd my-ens-script
npm init -y
mkdir src
```

Install [`tsx`](https://tsx.is/) so you can run TypeScript directly:

```sh
npm install -D tsx typescript @types/node
```

Add a `start` script to `package.json`:

```json title="package.json" ins={3-5}
{
  "type": "module",
  "scripts": {
    "start": "tsx src/index.ts"
  }
}
```

## 3. Write the query

Create `src/index.ts`. The whole script is a single `fetch` against `/api/omnigraph`.

```ts title="src/index.ts"
// you may use a NameHash Hosted ENSNode instance
// learn more at https://ensnode.io/docs/hosted-instances
const ENSNODE_URL = process.env.ENSNODE_URL ?? "https://api.v2-sepolia.ensnode.io";

const HELLO_WORLD_QUERY = /* GraphQL */ `
  query HelloWorld($name: InterpretedName!) {
    domain(by: { name: $name }) {
      __typename
      canonical { name { beautified } }
      owner { address }
      subdomains(first: 20) {
        totalCount
        edges { node { __typename canonical { name { beautified } } owner { address } } }
      }
    }
  }
`;

interface Domain {
  __typename: "ENSv1Domain" | "ENSv2Domain";
  canonical: { name: { beautified: string } } | null;
  owner: { address: string } | null;
}

interface QueryResult {
  data?: {
    domain:
      | (Domain & {
          subdomains: {
            totalCount: number;
            edges: { node: Domain }[];
          } | null;
        })
      | null;
  } | null;
  errors?: { message: string }[];
}

function formatDomain(domain: Domain): string {
  const name = domain.canonical?.name.beautified ?? "<unnamed>";
  const owner = domain.owner?.address ?? "0x0 (means reserved for ENSv2)";
  return `${name} (${domain.__typename}) — Owner ${owner}`;
}

async function main() {
  const response = await fetch(new URL("/api/omnigraph", ENSNODE_URL), {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      query: HELLO_WORLD_QUERY,
      variables: { name: "eth" },
    }),
  });

  if (!response.ok) {
    throw new Error(`Request failed: ${response.status} ${response.statusText}`);
  }

  const { data, errors } = (await response.json()) as QueryResult;

  if (errors) throw new Error(JSON.stringify(errors));
  if (!data?.domain) throw new Error("Domain 'eth' not found");

  const { domain } = data;
  const totalCount = domain.subdomains?.totalCount ?? 0;

  console.log(formatDomain(domain));
  console.log(`\nSubdomains (showing 20 of ${totalCount}):`);
  for (const { node } of domain.subdomains?.edges ?? []) {
    console.log(`  - ${formatDomain(node)}`);
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
```

A few things to notice:

- **`InterpretedName` is a scalar.** From the wire's perspective it's just a string — the server validates the format. Pass `"eth"` as a plain string in `variables`.
- **`subdomains` is a [Relay Connection](https://relay.dev/graphql/connections.htm).** Cursor through with `first`, `after`, `pageInfo { hasNextPage endCursor }` — same shape as any Relay-style API.
- **Hand-written types.** We're maintaining `interface Domain` and `interface QueryResult` ourselves here. If you want these generated and kept in sync with your queries automatically, use [`enssdk`](/docs/integrate/integration-options/enssdk).

## 4. Run it

```sh
ENSNODE_URL=https://api.v2-sepolia.ensnode.io npm start
```

You should see the `eth` Domain, its owner, and the first 20 of its subdomains.

## Where to go next

- Want typed queries with editor autocomplete and a real GraphQL client? Use [`enssdk`](/docs/integrate/integration-options/enssdk) — same API, with `gql.tada` types and an `EnsNodeClient`.
- Building a React app? Use [`enskit`](/docs/integrate/integration-options/enskit) — same `graphql(...)` helper plus `useOmnigraphQuery` and a graphcache.
- See [Omnigraph examples](/docs/integrate/omnigraph/examples) for ready-to-copy queries: account-owned domains, events, registrar permissions, full-text search, and more.
- See the [Omnigraph Schema Reference](/docs/integrate/omnigraph/schema-reference) for the full set of types, fields, and arguments you can query.

[omnigraph-graphql-example](https://github.com/namehash/ensnode/tree/main/examples/omnigraph-graphql-example)

[GraphQL specification](https://graphql.org/learn/)

[Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm)