Authentication
Two directions, two mechanisms. When you call us, you authenticate with your connection's key id and secret. What we send you is signed with YouGrow's own key, which you verify with our published public keys — so nothing you store could ever be used to impersonate YouGrow.
Your key id and secret
- Key id (
ygk_…) — public. It identifies your connection: it's your username for the API, and the audience of our tokens. - Secret (
ygs_…) — shown once when you create the connection. Keep it in your server's secret manager. Never put it in source control, a browser, a mobile app or logs. - Rotating: Settings → Rotate secret issues a new one. The previous secret keeps working for 24 hours, so you can deploy the new one without downtime. If a secret may have leaked, rotate immediately.
Authenticating your requests
Every request to the API uses HTTP Basic auth over HTTPS: your key id is the username and your secret is the password.
Authorization: Basic <base64 of "<key id>:<secret>"> curl -u "$YOUGROW_KEY_ID:$YOUGROW_SECRET" https://yougrow.ai/api/v2/me
- Most HTTP clients build the header for you:
curl -u,requests.patch(…, auth=(key_id, secret)), or the Node SDK fromkeyIdandsecret. - There's nothing to sign and no clock to keep in sync. HTTPS protects the request, and every write is idempotent, so a retry is harmless.
- A missing or wrong key id or secret gets
401— as does a revoked connection. During a rotation, the previous secret works for 24 hours. - Check a key with
GET /api/v2/me(yg.me()in the SDK). It returns the connection the key belongs to — its name, environment and status — so you can tell a staging key from a production one. - Send it from your server only. Anyone with the secret can change your users' state.
Verifying our requests
Context requests and webhooks carry Authorization: Bearer <JWT>, signed with ES256 by a key held in Google Cloud KMS. Verify it before trusting anything in the request:
- Read the header's
kidand find that key inhttps://yougrow.ai/.well-known/jwks.json. Cache the key set as itsCache-Controlallows; if a token names a key you don't have, fetch it again (at most once a minute). - Check the signature, and that
algisES256. Reject anything else, includingnoneand HMAC algorithms. - Check the claims:
| Field | Type | Notes |
|---|---|---|
| iss | string | Must be https://yougrow.ai. |
| aud | string | Must be your connection's key id. |
| dir | string | context on your context endpoint, webhook on your webhook endpoint. |
| exp / iat | number | Not expired (allow ~60 s of clock skew); tokens live at most 5 minutes. |
| body_sha256 | string | The base64url SHA-256 of the raw request body you received — so the body can't have been altered. |
| jti | string | Unique per request (the requestId or webhook id) — use it to drop repeats. |
With the Node SDK
import { createVerifier } from "@yougrowai/node/server";
const verifier = createVerifier({ keyId: process.env.YOUGROW_KEY_ID! });
const v = await verifier.verify({ headers: req.headers, rawBody, direction: "context" });
if (!v.ok) return res.status(401).end();It fetches and caches the key set, refetches when we rotate keys, and keeps working on cached keys if a refresh fails. New keys are published a day before they're used, so rotations never break you. Create it once, outside your handler, so the keys stay cached between requests.
Reading the raw body
Most frameworks parse JSON before your handler runs. Here's how to get the exact bytes in each — then verify, then parse. (verify takes a string, or a Buffer from 0.2.0.)
app.post("/yougrow/context", express.raw({ type: "application/json" }), async (req, res) => {
const rawBody = req.body.toString("utf8"); // a Buffer, because of express.raw()
// … verify, then JSON.parse(rawBody)
});export async function POST(req: Request) {
const rawBody = await req.text();
const v = await verifier.verify({ headers: req.headers, rawBody, direction: "context" });
if (!v.ok) return Response.json({ error: v.reason }, { status: 401 });
// … JSON.parse(rawBody)
}export const config = { api: { bodyParser: false } }; // keep the body unparsed
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
const rawBody = Buffer.concat(chunks).toString("utf8");
// … verify, then JSON.parse(rawBody)
}export const yougrowContext = onRequest(async (req, res) => {
const rawBody = req.rawBody.toString("utf8"); // req.body is already parsed; rawBody is the exact bytes
// … verify, then JSON.parse(rawBody)
});app.register(async (scope) => {
// For these routes only: hand JSON bodies over as the raw string.
scope.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) => done(null, body));
scope.post("/yougrow/context", async (req, reply) => {
const rawBody = req.body as string;
// … verify, then JSON.parse(rawBody)
});
});const app = await NestFactory.create(AppModule, { rawBody: true }); // main.ts
@Post("yougrow/context")
async context(@Req() req: RawBodyRequest<Request>) {
const rawBody = req.rawBody!.toString("utf8");
// … verify, then JSON.parse(rawBody)
}export const handler = async (event) => {
const rawBody = event.isBase64Encoded ? Buffer.from(event.body ?? "", "base64").toString("utf8") : event.body ?? "";
const v = await verifier.verify({ headers: event.headers, rawBody, direction: "context" });
// …
};Use a Node runtime: the SDK uses node:crypto, so it doesn't run on edge runtimes (Vercel Edge Functions, Next.js middleware, Cloudflare Workers). On a serverless platform, a cold start plus the first key fetch has to fit inside your context endpoint's timeout — keep the verifier at module scope, and consider keeping one instance warm.
Test vectors
test-vectors.json holds reference ES256 tokens, with the public key that verifies them, so you can check your verification in any language. The same file ships in the SDK package as test/vectors.json.