# Signing

> 

```ts
sign(payload, key, options?)
```

Produces a **compact JWS** — the familiar dotted three-part string.

## Parameters

<table>
<thead>
  <tr>
    <th>
      Name
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Required?
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        payload
      </code>
    </td>
    
    <td>
      <code>
        string | Uint8Array | Record<string, unknown>
      </code>
    </td>
    
    <td>
      Yes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        key
      </code>
    </td>
    
    <td>
      <code>
        CryptoKey | JWSSignJWK | Uint8Array
      </code>
    </td>
    
    <td>
      Yes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        options.alg
      </code>
    </td>
    
    <td>
      <code>
        JWSAlgorithm
      </code>
    </td>
    
    <td>
      When <code>
        key
      </code>
      
       has no <code>
        alg
      </code>
      
       hint
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        options.protectedHeader
      </code>
    </td>
    
    <td>
      <code>
        JWSHeaderParameters
      </code>
      
       (extra fields)
    </td>
    
    <td>
      No
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        options.expiresIn
      </code>
    </td>
    
    <td>
      <code>
        number | string
      </code>
      
       — e.g. <code>
        "1h"
      </code>
      
      , <code>
        "7D"
      </code>
      
      , <code>
        3600
      </code>
    </td>
    
    <td>
      No
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        options.currentDate
      </code>
    </td>
    
    <td>
      <code>
        Date
      </code>
    </td>
    
    <td>
      No
    </td>
  </tr>
</tbody>
</table>

`JWSSignJWK` narrows the JWK by family: `JWK_oct<JWK_HMAC>` for HMAC, or an asymmetric *private* JWK whose `alg` is in the matching signing family (`RS*`/`PS*` for RSA, `ES*` for ECDSA, `Ed25519`/`EdDSA` for OKP). A JWK whose `alg` points at a non-signing family (`"RSA-OAEP"`, `"A256KW"`, `"ECDH-ES"`, …) is rejected at the type level.

Returns `Promise<string>` — the compact JWS.

## Algorithm inference

When the key is a **JWK** with an `alg` field set, unjwt reads it and uses that algorithm. `generateJWK()` always sets `alg` for you, so in practice you rarely specify it by hand:

```ts [inferred.ts]
const key = await generateJWK("HS256"); // key.alg === "HS256"
const token = await sign({ sub: "u1" }, key); // uses HS256 automatically
```

You must pass `options.alg` **explicitly** in three cases:

- The key is a raw `Uint8Array` (no metadata to infer from).
- The key is a `CryptoKey` without algorithm context (rare — `generateKey()` sets the algorithm internally).
- The JWK has no `alg` field.

```ts [explicit.ts]
import { base64UrlDecode } from "unjwt/utils";

const rawKey = base64UrlDecode("GawgguFyGrWKav7AX4VKUg", { returnAs: "uint8array" });
const token = await sign({ sub: "u1" }, rawKey, { alg: "HS256" });
```

## `expiresIn` — setting `exp` declaratively

Sets the `exp` claim relative to `iat`:

```ts
await sign({ sub: "u1" }, key, { expiresIn: "1h" }); // exp = iat + 3600
await sign({ sub: "u1" }, key, { expiresIn: 30 }); // exp = iat + 30
await sign({ sub: "u1" }, key, { expiresIn: "7days" }); // exp = iat + 604800
```

Accepted forms: `"30s"`, `"10m"`, `"2h"`, `"7D"`, `"1W"`, `"3M"`, `"1Y"` and the long forms (`"seconds"`, `"minutes"`, `"hours"`, `"days"`, `"weeks"`, `"months"`, `"years"`).

If your payload already has an `iat`, it's preserved; otherwise the current time is used. Use `currentDate` to override "now" in tests.

<note>

`expiresIn` is a signer-side option. On the verifier side, pass `maxTokenAge` or rely on the `exp` claim that's already baked into the token.

</note>

## Custom header parameters

Anything passed in `protectedHeader` is merged into the JWS header. `alg` is reserved — it's always derived from the top-level `alg` option (or inferred from the key) and can't be overridden here. `b64` is user-settable: set it to `false` to opt into the RFC 7797 unencoded-payload mode (see below).

```ts [custom-header.ts]
const token = await sign({ sub: "u1" }, key, {
  protectedHeader: {
    kid: "legacy-2024", // overrides key.kid if set
    typ: "access+jwt", // RFC 8725 guidance — custom type name
    cty: "application/json", // nested-content media type
  },
});
```

### `kid` fallback

If the key is a JWK with a `kid` field and you don't set `protectedHeader.kid`, unjwt adds `kid` to the header automatically. This is the "make tokens carry their key identity" behavior — essential for JWKS endpoints and key rotation. An explicit `protectedHeader.kid` always wins.

## `b64: false` — RFC 7797 unencoded payload

[RFC 7797](https://www.rfc-editor.org/rfc/rfc7797) defines a variant where the JWS payload is signed over **raw bytes** rather than the base64url-encoded form. Useful when:

- The payload is already a large or structured document (XML, JSON-LD) that consumers want to read without base64 decoding.
- The payload contains binary data and base64url overhead matters.

```ts [unencoded.ts]
const token = await sign("important document bytes", key, {
  protectedHeader: { b64: false, crit: ["b64"] },
});
// Result: "eyJhbGc...crit...b64...".<raw payload>.<signature>
// (note the payload is literally in the middle)
```

Always pair `b64: false` with `crit: ["b64"]` so verifiers that don't recognize the parameter reject the token instead of mis-validating it.

<warning>

All signers of a multi-signature JWS must agree on `b64`. See [Multi-signature](/jwt/jws/multi-signature).

</warning>

## Payload types

`sign()` accepts three payload shapes:

<table>
<thead>
  <tr>
    <th>
      Shape
    </th>
    
    <th>
      Behavior
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Record<string, unknown>
      </code>
    </td>
    
    <td>
      Serialized to JSON. <code>
        typ: "JWT"
      </code>
      
       added automatically if not set.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      UTF-8 encoded as-is. No <code>
        typ
      </code>
      
       defaulting — you're not producing a JWT.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Uint8Array
      </code>
    </td>
    
    <td>
      Used as-is. No <code>
        typ
      </code>
      
       defaulting.
    </td>
  </tr>
</tbody>
</table>

The type is inferred so `verify()` returns the same shape (see [`forceUint8Array`](/jwt/jws/verifying) if you want bytes back).

## Full signature

```ts
interface JWSSignOptions {
  alg?: JWSAlgorithm;
  protectedHeader?: StrictOmit<JWSHeaderParameters, "alg"> & { alg?: never };
  currentDate?: Date;
  expiresIn?: ExpiresIn;
  expiresAt?: Date;
  notBeforeIn?: ExpiresIn;
  notBeforeAt?: Date;
}
```

`expiresIn` and `expiresAt` are mutually exclusive; `notBeforeIn` and `notBeforeAt` are likewise mutually exclusive. `notBeforeIn: 0` is allowed and sets `nbf = iat` (an explicit temporal floor at sign time).

## See also

- [Verifying →](/jwt/jws/verifying) — the consumer side.
- [Algorithms →](/jwt/jws/algorithms) — which `alg` to pick.
- [Multi-signature →](/jwt/jws/multi-signature) — more than one signer.
