# Quickstart

> 

This page walks you through a full sign/verify round trip, then a full encrypt/decrypt round trip. You can finish this page in two minutes just by copy-pasting the examples.

## Sign and verify a token

The simplest JWT is an HMAC-signed token — same key on both sides.

```ts [sign-verify.ts]
import { sign, verify } from "unjwt/jws";
import { generateJWK } from "unjwt/jwk";

// 1. Generate a symmetric key (once, at app start)
const key = await generateJWK("HS256");

// 2. Sign a payload (repeat per token)
const token = await sign({ sub: "user_1", role: "admin" }, key, { expiresIn: "1h" });
console.log(token);
// eyJhbGciOiJIUzI1NiIsImtpZCI6IjRj...

// 3. Verify and read the payload
const { payload, protectedHeader } = await verify(token, key);
console.log(payload);
// { sub: "user_1", role: "admin", iat: 1736860800, exp: 1736864400 }
console.log(protectedHeader);
// { alg: "HS256", typ: "JWT", kid: "4c3d..." }
```

What happened:

- [`generateJWK("HS256")`](/jwk/generating) returned a symmetric JWK (`kty: "oct"`) with a fresh `kid`.
- [`sign(...)`](/jwt/jws/signing) read the algorithm from `key.alg`, added `iat` (issued at) and `exp` (expiration) automatically because you passed `expiresIn: "1h"`, and produced a compact JWS string of the form `<header>.<payload>.<signature>`.
- [`verify(...)`](/jwt/jws/verifying) decoded the header, checked the signature, and validated the JWT claims (`exp`, `nbf`, `iat`) using the current clock.

<tip>

`expiresIn` accepts numbers (seconds) or strings like `"30s"`, `"10m"`, `"2h"`, `"7D"`, `"1W"`, `"3M"`, `"1Y"`. Same format everywhere in unjwt.

</tip>

## Encrypt and decrypt a token

When the payload itself is sensitive (contains a PII, a secret, anything the client shouldn't read), use encryption instead of signing.

```ts [encrypt-decrypt.ts]
import { encrypt, decrypt } from "unjwt/jwe";

// 1. Encrypt with a password — unjwt uses PBES2 under the hood
const jwe = await encrypt({ creditCard: "4242...", balance: 10_000 }, "my-strong-password");
console.log(jwe);
// eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ci...

// 2. Decrypt with the same password
const { payload } = await decrypt(jwe, "my-strong-password");
console.log(payload);
// { creditCard: "4242...", balance: 10000 }
```

The payload is now **unreadable** without the password. A JWE token still looks like dotted base64 segments, but the third segment onwards is ciphertext — no amount of base64 decoding gets you the claims back.

<note>

Password-based encryption is the easiest way in, but PBKDF2 is intentionally slow (default 600,000 iterations per [OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2)). For high-throughput workloads, use a [symmetric or asymmetric key](/jwt/jwe/algorithms) instead.

</note>

## Where next?

You've now seen the two main workflows. Pick whichever matches what you're building:

- Confused about **JWT vs JWS vs JWE vs JWK**? → [Core concepts](/getting-started/core-concepts).
- Building an **API with login tokens**? → [JWS overview](/jwt/jws) and the [authentication-basics example](/examples/authentication-basics).
- Need to **encrypt data at rest or in transit**? → [JWE overview](/jwt/jwe).
- Working with **third-party tokens** (OAuth, OIDC, JWKS endpoints)? → [Verifying with a key lookup](/jwt/jws/verifying#dynamic-key-resolution-jwklookupfunction) and [JWK Sets](/jwk/jwk-sets).
- Building on **H3, Nuxt, or Nitro**? → [H3 session adapters](/adapters).
