---
title: getHookByToken
description: Retrieve hook details and workflow run information by token.
type: reference
summary: Use getHookByToken to look up a hook's metadata and associated workflow run before resuming it.
prerequisites:
  - /docs/foundations/hooks
related:
  - /docs/foundations/idempotency
---

Retrieves a hook by its unique token, returning the associated workflow run information and any metadata that was set when the hook was created. This function is useful for inspecting hook details before deciding whether to resume a workflow.

When `experimental_minRetention` is set, this function continues to return the Hook after its workflow ends until retention ends. That Hook cannot be resumed. Use `getRun(hook.runId)` to inspect the finished run.

When the Hook was created with [`experimental_force`](/docs/api-reference/workflow/create-hook#take-over-a-token-another-run-holds) and took its token from another run, `hook.claimedFrom` names that run and its Hook. A lookup always follows the token to its current owner, so the same token returns the new Hook as soon as the takeover happens.

<Callout type="warn">
`getHookByToken` is a runtime function that must be called from outside a workflow function.
</Callout>

<Callout type="info">
`hook.metadata` is a getter that returns a Promise, so `await` it to read the value. Hydrating metadata can add extra network round trips, so that work is deferred to first access and the lookup itself stays a single read. Awaiting it on a hook with no metadata resolves `undefined` and performs no extra work, and repeat reads are free.
</Callout>

<Callout type="info">
Looking up a deterministic hook token is useful in hook-based idempotency flows, but it is only an advisory check. If no hook exists yet, another request can still start the same workflow before your `start()` call registers its hook. Use the lookup to avoid obvious duplicate starts, and handle the race inside the workflow by checking `await hook.getConflict()` before duplicate-sensitive work. On a conflict it resolves with the run that owns the token, so the duplicate can route the caller to the active owner. If duplicates must be rejected before a workflow body runs, keep a durable request record until native atomic start-and-hook registration exists. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
</Callout>

```typescript lineNumbers
import { getHookByToken } from "workflow/api";

export async function POST(request: Request) {
  const { token } = await request.json();
  const hook = await getHookByToken(token);
  console.log("Hook belongs to run:", hook.runId);
}
```

## API signature

### Parameters

<TSDoc
definition={`
import { getHookByToken } from "workflow/api";
export default getHookByToken;`}
showSections={["parameters"]}
/>

### Returns

Returns a `Promise<Hook>` that resolves to:

<TSDoc
definition={`
import type { Hook } from "workflow/api";
export default Hook;`}
showSections={["returns"]}
/>

## Examples

### Basic hook lookup

Retrieve hook information before resuming:

```typescript lineNumbers
import { getHookByToken, resumeHook } from "workflow/api";

export async function POST(request: Request) {
  const { token, data } = await request.json();

  try {
    // First, get the hook to inspect its metadata
    const hook = await getHookByToken(token); // [!code highlight]

    console.log("Resuming workflow run:", hook.runId);
    console.log("Hook metadata:", await hook.metadata); // [!code highlight]

    // Then resume the hook with the payload
    await resumeHook(token, data);

    return Response.json({
      success: true,
      runId: hook.runId
    });
  } catch (error) {
    return new Response("Hook not found", { status: 404 });
  }
}
```

### Validating hook before resume

Use `getHookByToken` to validate hook ownership or metadata before resuming. Take the user's identity from your authentication layer, not from the request body. See [Hook and webhook security](/docs/foundations/hooks#security).

```typescript lineNumbers
import { getHookByToken, resumeHook } from "workflow/api";

declare function getSession(request: Request): Promise<{ userId: string } | null>; // @setup

export async function POST(request: Request) {
  const session = await getSession(request);
  if (!session) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }
  const { token, data } = await request.json();

  try {
    const hook = await getHookByToken(token); // [!code highlight]
    // `metadata` is a Promise, so awaiting it hydrates the stored value.
    const metadata = (await hook.metadata) as { allowedUserId?: string } | undefined; // [!code highlight]

    // Validate that the hook metadata matches the user
    if (metadata?.allowedUserId !== session.userId) {
      return Response.json(
        { error: "Unauthorized to resume this hook" },
        { status: 403 }
      );
    }

    await resumeHook(token, data);
    return Response.json({ success: true, runId: hook.runId });
  } catch (error) {
    return Response.json({ error: "Hook not found" }, { status: 404 });
  }
}
```

### Checking hook environment

Verify the hook belongs to the expected environment:

```typescript lineNumbers
import { getHookByToken, resumeHook } from "workflow/api";

export async function POST(request: Request) {
  const { token, data } = await request.json();
  const expectedEnv = process.env.VERCEL_ENV || "development";

  try {
    const hook = await getHookByToken(token); // [!code highlight]

    if (hook.environment !== expectedEnv) {
      return Response.json(
        { error: `Hook belongs to ${hook.environment} environment` },
        { status: 400 }
      );
    }

    await resumeHook(token, data);
    return Response.json({ runId: hook.runId });
  } catch (error) {
    return Response.json({ error: "Hook not found" }, { status: 404 });
  }
}
```

### Logging hook information

Log hook details for debugging or auditing:

```typescript lineNumbers
import { getHookByToken, resumeHook } from "workflow/api";

export async function POST(request: Request) {
  const url = new URL(request.url);
  const token = url.searchParams.get("token");

  if (!token) {
    return Response.json({ error: "Missing token" }, { status: 400 });
  }

  try {
    const hook = await getHookByToken(token); // [!code highlight]

    // Log for auditing
    console.log({
      action: "hook_resume",
      runId: hook.runId,
      hookId: hook.hookId,
      projectId: hook.projectId,
      createdAt: hook.createdAt,
    });

    const body = await request.json();
    await resumeHook(token, body);

    return Response.json({ success: true });
  } catch (error) {
    return Response.json({ error: "Hook not found" }, { status: 404 });
  }
}
```

## Related functions

- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): Resume a hook with a payload.
- [`createHook()`](/docs/api-reference/workflow/create-hook): Create a hook in a workflow.
- [`defineHook()`](/docs/api-reference/workflow/define-hook): Type-safe hook helper.
- [Idempotency](/docs/foundations/idempotency): Deduplicate step side effects and workflow starts.
