> For the complete documentation index, see [llms.txt](https://docs.goassay.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.goassay.ai/developers/list-your-api-get-started/verify-in-your-code.md).

# Verify in your code (Node, Python, Go)

Add signature verification inside your service with the Node, Python or Go library.

An alternative to the [Cloudflare Worker](/developers/list-your-api-get-started/cloudflare-worker.md) when you can change your service. The middleware verifies the raw request before your handler runs, then makes the body available to your application.

## How it works

The library fetches Assay's public keys from `https://api.goassay.ai/.well-known/assay-request-keys.json`. It checks the signature, body digest, time window and nonce. A nonce is a one-time identifier used to detect a repeated signed request.

Verify before parsing the body or rewriting the request target. Your public endpoint must use HTTPS and answer directly without redirects. If a proxy changes `Host`, configure the verifier's authority option with the public hostname, including a non-default port when needed.

## Example

These minimal servers protect every route and return `{"ok":true}` for a valid call to `/v1/search`. Replace that response with your tool's implementation. They listen locally on port `8080`; deploy behind HTTPS while preserving the signed request parts.

{% tabs %}
{% tab title="Node" %}
Requires Node 20 or newer. In your application directory:

```sh
npm install @goassay-ai/provider-verify express
```

Save as `server.mjs`, then run `node server.mjs`:

```js
import express from "express";
import { assayVerify } from "@goassay-ai/provider-verify/express";

const app = express();
app.use(assayVerify()); // Before body parsers and handlers.
app.post("/v1/search", (req, res) => {
  res.json({ ok: true });
});
app.listen(8080, "127.0.0.1");
```

The middleware sets `req.rawBody`. For JSON content types it also parses `req.body`; other bodies are left as a `Buffer`. For a proxy that rewrites the host, use `assayVerify({ authority: "api.example.com" })`.
{% endtab %}

{% tab title="Python" %}
Requires Python 3.9 or newer. In a virtual environment:

```sh
python -m pip install assay-provider-verify
```

Save as `server.py`, then run `python server.py`. This uses Python's local WSGI server; deploy the wrapped application with your production WSGI server.

```python
from wsgiref.simple_server import make_server
from assay_provider_verify import AssayVerifyWSGI

def handler(environ, start_response):
    if environ["REQUEST_METHOD"] != "POST" or environ["PATH_INFO"] != "/v1/search":
        start_response("404 Not Found", [("Content-Type", "application/json")])
        return [b'{"error":"not_found"}']
    start_response("200 OK", [("Content-Type", "application/json")])
    return [b'{"ok":true}']

application = AssayVerifyWSGI(handler)

if __name__ == "__main__":
    with make_server("127.0.0.1", 8080, application) as server:
        server.serve_forever()
```

The wrapper restores the body in `wsgi.input`. For a proxy that rewrites the host, use `AssayVerifyWSGI(handler, authority="api.example.com")`.
{% endtab %}

{% tab title="Go" %}
Requires Go 1.21 or newer. In a new application directory:

```sh
go mod init example.com/assay-provider
go get github.com/assay-ai/assay-provider-verify/go@latest
```

Save as `main.go`, then run `go run .`:

```go
package main

import (
    "io"
    "log"
    "net/http"

    assayverify "github.com/assay-ai/assay-provider-verify/go"
)

func main() {
    verify, err := assayverify.Middleware(assayverify.Options{})
    if err != nil {
        log.Fatal(err)
    }
    mux := http.NewServeMux()
    mux.HandleFunc("/v1/search", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            w.WriteHeader(http.StatusMethodNotAllowed)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        _, _ = io.WriteString(w, `{"ok":true}`)
    })
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", verify(mux)))
}
```

The middleware restores `r.Body`. For a proxy that rewrites the host, set `assayverify.Options{Authority: "api.example.com"}`.
{% endtab %}
{% endtabs %}

### Other frameworks

Each adapter takes the same options (`authority`, `replayStore` or `replay_store`, and so on).

| Framework             | Use                                                                                                                                                           |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Fastify               | `import { assayVerifyPlugin } from "@goassay-ai/provider-verify/fastify"`, then `app.register(assayVerifyPlugin, {})`. It covers every route.                 |
| Hono                  | `import { assayVerify } from "@goassay-ai/provider-verify/hono"`, then `app.use("*", assayVerify())`.                                                         |
| Next.js route handler | `import { withAssayVerify } from "@goassay-ai/provider-verify/next"`, then `export const POST = withAssayVerify(async (req) => Response.json({ ok: true }))`. |
| FastAPI or Starlette  | `python -m pip install "assay-provider-verify[fastapi]"`, then `app.add_middleware(AssayVerifyMiddleware)`.                                                   |
| Flask                 | `python -m pip install "assay-provider-verify[flask]"`, then `app.wsgi_app = AssayVerifyWSGI(app.wsgi_app)`.                                                  |

Python's `AssayVerifyMiddleware` and `AssayVerifyWSGI` both import from `assay_provider_verify`.

## Check your setup locally

Before you deploy, send a valid signed request to your middleware. The verifier repository publishes signed test requests in `test-vectors/v1.json`. This Node script mounts the Express middleware with that file's test key and clock, then sends three requests.

In the directory where you installed `@goassay-ai/provider-verify` and `express`:

```sh
curl -fsSLO https://raw.githubusercontent.com/assay-ai/assay-provider-verify/main/test-vectors/v1.json
```

Save as `local-check.mjs`, then run `node local-check.mjs`:

```js
import { readFileSync } from "node:fs";
import express from "express";
import { assayVerify } from "@goassay-ai/provider-verify/express";

const v = JSON.parse(readFileSync("v1.json", "utf8"));
const c = v.cases[0]; // A signed POST /v1/search?q=a%20b

const app = express();
app.use(assayVerify({
  // Test-only: the public fixture key, its fixed clock and its host.
  jwks: { keys: [v.publicKeyJwk] },
  now: () => c.params.created,
  authority: "api.example.test",
}));
app.post("/v1/search", (req, res) => res.json({ ok: true, got: req.body }));

const server = app.listen(0, "127.0.0.1", async () => {
  const { pathname, search } = new URL(c.request.url);
  const url = `http://127.0.0.1:${server.address().port}${pathname}${search}`;
  const signed = {
    method: c.request.method,
    headers: { ...c.request.headers, ...c.expected.headers, "content-type": "application/json" },
    body: c.request.body,
  };
  for (const [label, init] of [
    ["signed", signed],
    ["same request again", signed],
    ["unsigned", { method: "POST", body: c.request.body }],
  ]) {
    const res = await fetch(url, init);
    console.log(label, res.status, await res.text());
  }
  server.close();
});
```

Expected output:

```
signed 200 {"ok":true,"got":{"q":"a b"}}
same request again 401 {"error":"invalid_assay_signature","reason":"REPLAYED"}
unsigned 401 {"error":"invalid_assay_signature","reason":"MISSING"}
```

To check your real handler, replace the `/v1/search` route with yours. Python (`jwks=`, `now=`, `authority=`) and Go (`JWKS`, `Now`, `Authority`) accept the same test options.

{% hint style="danger" %}
The fixture key is public, and the fixed clock turns off expiry. Use them only in tests. A live server must use the default key set and the real clock.
{% endhint %}

## Connect and check it worked

1. Sign in to Assay and open your listing's **Connect & test** step.
2. Set **Your API's address** to the public HTTPS endpoint running the middleware.
3. Under **Authentication**, select **Assay signature (recommended)**. If you choose **Both**, your service must also enforce the key; use the [API key setup steps](/developers/list-your-api-get-started/api-key-or-signature.md#set-up-an-api-key).
4. Fill in the tool's test input and click **Save and run test**, or **Run the test again**.

The signed call should pass, with no **Signature not enforced** warning. An unsigned call to the same protected route must return `401` with `{"error":"invalid_assay_signature","reason":"MISSING"}`.

## Test with the test vectors

Get a copy of the [verifier repository](https://github.com/assay-ai/assay-provider-verify). Run the matching suite from its root:

{% tabs %}
{% tab title="Node" %}

```sh
npm --prefix node ci
npm --prefix node test -- test/vectors.test.ts
```

{% endtab %}

{% tab title="Python" %}
In a virtual environment:

```sh
python -m pip install './python[test]'
python -m pytest python/tests/test_vectors.py
```

{% endtab %}

{% tab title="Go" %}

```sh
cd go
go test -run 'Test(Positive|Negative)Vectors' ./...
```

{% endtab %}
{% endtabs %}

`test-vectors/v1.json` contains valid signed requests. The suite combines each case's request headers with `expected.headers`, uses `publicKeyJwk` as the key set and pins the clock to `params.created`.

`test-vectors/negative.json` gives `now` and `expectedReason` for each request. Each case starts with a fresh replay store. A `sequence` keeps the same store between steps, so the replay case accepts once and then returns `REPLAYED`. `OK` means acceptance, even in this file.

When testing your own integration, use those same inputs and raw UTF-8 body bytes.

## Details

| Check or setting       | Default                                                                                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Signed validity window | At most 300 seconds, with 60 seconds of clock tolerance on either side.                                                                                      |
| Buffered body cap      | 5 MiB (5,242,880 bytes). Larger bodies get `413 {"error":"payload_too_large"}` before verification.                                                          |
| Key fetching           | 5-second timeout; keys cached for one hour. An unknown key can trigger a refetch at most once per 60 seconds; failed fetches have a 5-second retry pause.    |
| Replay storage         | In memory. Node's middleware owns a store; Python and Go use a process-wide default. Mount and reuse the middleware. Separate instances do not share memory. |

With multiple service instances, supply a shared replay store to each verifier. If that store fails, keep the request rejected. Do not fall back to accepting it.

## Errors

| Symptom                                                  | Fix                                                                       |
| -------------------------------------------------------- | ------------------------------------------------------------------------- |
| Express returns `500` with `assay_verify_misconfigured`. | Mount `assayVerify()` before body parsers.                                |
| `DIGEST_MISMATCH`                                        | Verify the original bytes before a parser or proxy changes them.          |
| `BAD_SIGNATURE` behind a proxy                           | Preserve the target and configure the public authority if `Host` changes. |

## Go further

* List your API (optional): [Or: publish your own API](https://docs.goassay.ai/list-your-api/publish-your-own-api).

## Next

* [Reject reasons](/developers/list-your-api-get-started/reject-reasons.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.goassay.ai/developers/list-your-api-get-started/verify-in-your-code.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
