Trusted integration boundary

Secrets and API Calls

Secrets stay server-side. This guide details safe call patterns so credentials never leak into client-visible code or logs.

Zero-gap secret usage patterns for secure outbound integrations.

Implementation focus

Apply these rules before integrating external APIs, OAuth, or webhook signatures.

Expected outcomes

What Secrets Protect

Secrets let Pulses call credential-backed providers without putting API keys, tokens, or credentials in browser code or public source. Start with policy-bound auth handles on env.fetch() so Vibecodr can attach the credential server-side.

Stored privately

Secret values are write-only in product surfaces and are not returned in UI or API responses.

Server-side use

Policy-bound requests attach credentials on the server side instead of exposing plaintext values to public runtime code.

Host Allowlists

Optional restriction to specific domains for defense-in-depth.

Plan Availability

Plan

Secrets Enabled

Max Secrets

Free

No

0

Creator

Yes

25

Pro

Yes

200

Creator+

Secrets are available on Creator and Pro plans. Limits are shared across My Secrets and Pulse Overrides.

Adding Secrets

Secrets can be managed in Studio (Configure → API → Secrets) or in the Pulse Operating Center under the Secrets tab:

Global Secrets (My Secrets)

Available to all your pulses automatically. Great for API keys you use across multiple projects (e.g., STRIPE _SECRET _KEY).

Where: Studio → Configure → API → Secrets (or Pulse Operating Center → Secrets → My Secrets)

Pulse Overrides (Optional)

Override a global secret for a specific pulse. Useful when one pulse needs a different API key than the others.

Where: Studio → Configure → API → Secrets (or Pulse Operating Center → Secrets → Pulse Overrides)

Tip: Start with global secrets when several Pulses use the same provider key. Add overrides only when a specific Pulse needs a different value for the same key.

Naming convention: Use SCREAMING_SNAKE_CASE for secret keys (e.g., STRIPE _SECRET _KEY, GITHUB_TOKEN).

Allowed hosts: Specify which hosts can receive the secret (e.g., api.stripe.com) for defense-in-depth security. Without allowed hosts, secrets can be used with any public URL that passes Pulse egress policy. Prefer policy-bound auth handles over raw token compatibility paths.

Using Secrets in Code

Start with env.fetch(..., { auth: env.secrets.bearer("KEY") }) for secret-backed calls. It keeps the secret value out of your code and routes the outbound request through policy fetch. Compatibility token helpers exist for older code, but beginner docs use policy-bound auth handles.

Compatibility token scanning only inspects application/json, application/x-www-form-urlencoded, and text/* bodies. Binary or multipart bodies fail closed when opaque handles are present. Request bodies are scanned up to 1MB; larger payloads fail closed with 413.

Response limits when using secrets - If secrets are injected, Vibecodr only returns redaction-safe text responses (for example JSON) up to 10MB. Non-text responses (like application/octet-stream) fail closed with 415 egress.unredactableContentType. Oversized responses fail closed with 413 egress.responseTooLarge.

If you need to download a binary with auth, use a two-step flow: call an API with env.fetch(..., { auth: env.secrets.bearer("KEY") }) to request a signed URL (JSON/text), then fetch that URL with normal env.fetch() (no secrets involved).

Basic example: Stripe API

import { definePulse } from "@vibecodr/pulse";

export default definePulse(async (input, env) => {
  const response = await env.fetch("https://api.stripe.com/v1/customers", {
    method: "POST",
    auth: env.secrets.bearer("STRIPE_SECRET_KEY"),
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      email: input.email || "user@example.com",
    }),
  });

  return await response.json();
});

Policy-bound auth helpers

Bearer

Sends a secret as Authorization: Bearer

env.secrets.bearer("KEY")

Header

Sends a secret through a named header

env.secrets.header("KEY", "X-API-Key")

Query

Uses a secret-backed query parameter

env.secrets.query("KEY", "apikey")

Safety tip: Prefer bearer or header auth. Use query auth only when the provider requires it, since URLs are commonly logged by servers, proxies, and analytics. Auth handles are not secret strings; do not store, copy, or log them.

Common Patterns

Bearer Token (Stripe, GitHub, etc.)

const response = await env.fetch("https://api.stripe.com/v1/customers", {
  method: "POST",
  auth: env.secrets.bearer("STRIPE_SECRET_KEY"),
});

API Key in Query String (Weather APIs, etc.)

const response = await env.fetch("https://api.weather.com/forecast?city=NYC", {
  auth: env.secrets.query("WEATHER_API_KEY", "apikey"),
});

Custom Header (Stripe, etc.)

const response = await env.fetch("https://api.example.com/private", {
  auth: env.secrets.header("PRIVATE_API_TOKEN", "Authorization", "Basic {secret}"),
});

Signed Webhook Verification

Pulse webhooks are provider-generic. env.webhooks.verify("stripe",...) is the first certified provider helper, not the whole webhook system. For GitHub, Shopify, Slack, and other signed webhooks, use env.secrets.verifyHmac(...) until a provider-specific helper ships with full fixtures.

Provider helper

Use env.webhooks.verify("stripe") when you want a typed Stripe event and Vibecodr-owned raw-body verification.

HMAC preset

Use format presets for signed providers that share a known HMAC shape, while still keeping the secret out of your code.

Custom signature

Use signature: { value, encoding, prefix } for advanced providers that do not match a preset.

Format

Headers

Signed bytes

github-sha256

x-hub-signature-256

Exact raw body, hex signature

shopify-hmac-sha256

x-shopify-hmac-sha256

Exact raw body, base64 signature

slack-v0

x-slack-signature

+

x-slack-request-timestamp

v0:<timestamp>:<raw body>

, hex signature

Stripe provider helper

import { definePulse } from "@vibecodr/pulse";

export default definePulse(async (_input, env) => {
  const event = await env.webhooks.verify("stripe", {
    secret: "STRIPE_WEBHOOK_SECRET",
    maxBytes: 256 * 1024,
  });

  // Safe to trust after verification.
  return Response.json({ received: true, type: event.type });
});

Generic HMAC preset

import { definePulse } from "@vibecodr/pulse";

export default definePulse(async (_input, env) => {
  const rawBody = await env.request.raw.arrayBuffer({ maxBytes: 256 * 1024 });
  const verified = await env.secrets.verifyHmac("GITHUB_WEBHOOK_SECRET", {
    format: "github-sha256",
    message: rawBody,
    signatureHeader: env.request.headers.get("x-hub-signature-256"),
    maxBytes: 256 * 1024,
  });

  if (!verified.ok) {
    return new Response("unauthorized", { status: 401 });
  }

  const payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(rawBody));
  return Response.json({ accepted: true, action: payload.action ?? "unknown" });
});

Advanced custom signature

const rawBody = await env.request.raw.arrayBuffer({ maxBytes: 256 * 1024 });
const verified = await env.secrets.verifyHmac("WEBHOOK_SECRET", {
  algorithm: "sha256",
  message: rawBody,
  signature: {
    value: env.request.headers.get("x-provider-signature"),
    encoding: "hex",
    prefix: "sha256=",
  },
  maxBytes: 256 * 1024,
});

if (!verified.ok) {
  return new Response("unauthorized", { status: 401 });
}

Fail closed: read the exact bounded raw body, verify the signature, and parse JSON only after verified.ok. Unsupported provider helpers fail before trusted parsing or outbound work, and diagnostics use safe reasons such as missing_signature, unsupported_format, message_too_large, or stale_timestamp.

Helper Methods

Check if secrets exist before using them:

import { definePulse } from "@vibecodr/pulse";

export default definePulse(async (input, env) => {
  // Check if a secret is configured
  if (!env.secrets.has("STRIPE_SECRET_KEY")) {
    return { error: "Please configure STRIPE_SECRET_KEY in Studio (Configure -> API -> Secrets) or Pulse Operating Center" };
  }

  // List all available secret keys (not values!)
  const keys = env.secrets.keys();
  env.log.info("Available secrets:", keys);
  // Output: ["STRIPE_SECRET_KEY", "GITHUB_TOKEN"]

  // Proceed with your request...
});

Host Allowlists (Optional)

For extra security, you can restrict a secret to specific domains. If set, the secret can only be used with requests to those hosts.

Exact Match

api.stripe.com

Only matches this exact domain

Wildcard

*.stripe.com

Matches api.stripe.com, dashboard.stripe.com, etc.

No allowlist? If you leave it empty, the secret can be used with any public host. The UI shows an amber "No host restrictions" badge as a reminder.

Troubleshooting

Secret not found (404)

Host blocked (403)

Secrets disabled

Security Boundaries

Policy-Bound Secret Use

Passing raw secrets to application code makes accidental logging or exfiltration easier. Vibecodr's recommended path keeps credential attachment inside policy-mediated outbound requests:

  1. You call env.fetch with a policy-bound secret auth handle
  2. Your request is mediated by Vibecodr policy, which enforces the infra blocklist and SSRF checks
  3. If allowedHosts is configured, Vibecodr verifies the target URL is allowed
  4. The policy layer attaches the credential to the outbound request
  5. The response comes back to your code without returning the secret value

Result: With policy-bound auth handles, your code works with the provider response instead of a plaintext secret string. Avoid advanced opaque-token compatibility helpers unless a provider shape truly requires them.

Defense in Depth: Configure allowedHosts (e.g., ["api.stripe.com"]) to restrict where secrets can be sent. Without this, secrets can be proxied to any public URL. SSRF protection (blocking private IPs/metadata endpoints), the infra blocklist, and redirect re-validation are always active regardless.

Secret boundary

Secrets belong in trusted backend code, not in vibe source, runtime bundles, client-side logs, query strings, public metadata, screenshots, or examples that users are likely to copy into browser code.

Pulse code can use owner-managed secrets to call external APIs, but the response should only contain data the viewer is allowed to see.

  • Store provider keys in the secret manager, not source files.
  • Restrict host usage when the platform exposes host restrictions.
  • Never echo secrets in errors or debug output.
  • Rotate credentials if they were ever pasted into public code.

External API calls

External API calls that require credentials should run in Pulses. Browser-side calls are appropriate only for public APIs or user-provided credentials that are intentionally visible to that browser session.

Use env.fetch with env.secrets.bearer(), env.secrets.header(), or env.secrets.query() when a provider request needs an API key. That keeps the credential out of source code and lets the runtime attach it through policy-controlled outbound fetch.

Treat every outbound integration as a trust boundary: validate input, constrain destinations, handle provider errors, and return a safe response shape.

  • Keep API tokens server-side.
  • Normalize provider errors before returning them to clients.
  • Avoid storing raw provider payloads unless the product requires it.

Example and read next

Example: a vibe needs a private API key. Store the key as a Pulse secret, call the provider from the Pulse, and return only the app-level result that the browser actually needs.

Use these related pages when you need the next layer of guidance. They point to the most likely follow-up tasks, not every page that happens to touch the same system.

Related documentation