Docs · Vibes

Vibes, and Vibes Pay for your app

Vibes are OpenVibe's social currency: whole numbers, bought with money, spent on tips, cheers, paid messages, consumer tools and in-app items. They are never earned, and they are not cash.

Preview. These surfaces open when OpenVibe enables Billing's money settings. The sandbox works now and never touches real money.

What Vibes are

One Vibes price is a whole number, set by the app or the product: 50 Vibes, 1,250 Vibes. A person gets Vibes with Get Vibes (a pack with a card, or from their Balance) and spends them. Vibes are never earned: OpenCoins are the reward currency. Vibes cannot be moved back into Balance; a creator or app that receives Vibes cashes them out under the payout rules below.

Use Vibes when the price is social and simple — a tip, a cheer, a paid message, an image or another consumer tool, an item in your app. Use Balance (dollars, exact to the nano-dollar) for metered and professional usage: see the Balance guide. A product never asks a person to choose between the two for the same thing.

Vibes Pay step by step

  1. Your server creates a charge with your app token (capability billing.vibes.charge): amount in whole Vibes, a description, your reference (the order id, and the idempotency key) and a return_url. Billing answers a pay_url and a charge id.
  2. The person pays on OpenVibe's sheet at pay_url. It shows your app and the item, the price in Vibes and their Vibes, with one button — “Pay 50 Vibes”, or “Get Vibes” when they are short. It never shows a card form: the Vibes are already bought.
  3. Billing moves the Vibes, atomically and once, from the person to your project's Vibes wallet. The charge becomes paid.
  4. Your server learns it from a signed webhook, and can always ask GET /api/v1/vibes/charges/:id. Fulfil only after one of those says paid; never trust the page's word alone.
  5. Refund, pay out and watch the limits as described below.

In the browser, openvibe-shared/vibes-pay.js opens pay_url and resolves { status } from a postMessage whose origin is https://openvibe.network:

Browser · vibes-pay.js
<script src="https://openvibe.network/shared/vibes-pay.js" defer></script>

<button data-ov-vibes-pay="https://openvibe.network/pay/vch_…">Pay with Vibes</button>

<script>
  const { status } = await OpenVibe.vibes.pay('https://openvibe.network/pay/vch_…');
  if (status === 'paid') showThanks();
</script>

The button binds itself in the Vibes accent and both themes. The page result is a hint to refresh; the server's webhook or charge read is the confirmation.

Server SDK

openvibe-sdk/vibes is in openvibe-sdk v0.84.0 and later:

Node · openvibe-sdk/vibes
const { createVibesClient } = require('openvibe-sdk/vibes');

const vibes = createVibesClient({
  appToken: process.env.OPENVIBE_APP_TOKEN,      // capability billing.vibes.charge
  billingUrl: 'https://billing.openvibe.network',
});

const charge = await vibes.charges.create({
  amount: 50,                                     // whole Vibes, never a float
  description: 'Sticker pack: Neon cats',
  reference: 'order_123',                          // your order id; also the Idempotency-Key
  return_url: 'https://myapp.example/thanks',
});
// { id: 'vch_…', pay_url: 'https://openvibe.network/pay/vch_…', status: 'pending', expires_at: '…' }

const current = await vibes.charges.get(charge.id); // { status: 'paid', … }
await vibes.charges.refund(charge.id, { reason: 'requested by the buyer' });

The app token stays on your server. A token that expires can be supplied as a tokenProvider (a function, or an SDK token client with getToken()/invalidate()); a 401 invalidates it and retries once with the same Idempotency-Key. A float, zero or a negative amount throws before anything is sent. Billing's problem+json becomes a VibesError with status, code, body and retryable.

Plain HTTP

Everything the SDK does is these three requests. The app token carries billing.vibes.charge; the person's token is not used by your server.

Create a charge
POST https://billing.openvibe.network/api/v1/vibes/charges
Authorization: Bearer $OPENVIBE_APP_TOKEN
Content-Type: application/json
Idempotency-Key: order_123

{
  "amount": 50,
  "description": "Sticker pack: Neon cats",
  "reference": "order_123",
  "return_url": "https://myapp.example/thanks"
}

→ 201
{ "id": "vch_…", "pay_url": "https://openvibe.network/pay/vch_…", "status": "pending", "expires_at": "…" }
Read a charge
GET https://billing.openvibe.network/api/v1/vibes/charges/vch_…
Authorization: Bearer $OPENVIBE_APP_TOKEN

→ 200 { "id": "vch_…", "amount": 50, "status": "paid", "reference": "order_123", … }
Refund a charge
POST https://billing.openvibe.network/api/v1/vibes/charges/vch_…/refund
Authorization: Bearer $OPENVIBE_APP_TOKEN
Content-Type: application/json

{ "reason": "requested by the buyer" }

The reference is the idempotency key. The same reference with the same request is the same charge; changing the request under it is refused, never charged twice. A create whose answer was lost can be retried safely.

Webhooks and their signature

Register an HTTPS endpoint for your app. Billing sends a signed delivery on paid and refunded (event types billing.vibes.charge.paid and billing.vibes.charge.refunded), retried with backoff until your server answers 2xx.

Every delivery carries X-OpenVibe-Signature-V2:

t=<unix seconds>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">

Verify the raw bytes you received, not a re-serialised JSON object, with the app's webhook secret. Check the signature in constant time, reject a timestamp more than 300 seconds from your clock, and return 2xx quickly. The legacy X-OpenVibe-Signature (sha256=<hex> of the body alone) is still accepted by the SDK for receivers that have not moved; it carries no time and can be replayed, so use v2.

Node · verify the raw body
const express = require('express');
const { verifyWebhook } = require('openvibe-sdk/vibes');
const app = express();

app.post('/webhooks/vibes', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyWebhook(
    req.body,                                    // a Buffer: the raw body
    req.get('X-OpenVibe-Signature-V2'),
    process.env.OPENVIBE_VIBES_WEBHOOK_SECRET,
  );
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  // Mark the order paid by event.data.reference (or the charge id); the same delivery may arrive twice.
  markPaid(event);
  res.sendStatus(204);
});

The same check is vibes.webhooks.verify(rawBody, signatureHeader, secret) on the SDK client — pass the raw bytes, never a re-serialised object. Delivery is at least once: key your work on the charge id (or your reference) and do nothing when it is already paid. If a delivery never arrives, poll GET /api/v1/vibes/charges/:id; that read is the same authority as the webhook.

Sandbox

A sandbox app creates sandbox charges, paid with sandbox Vibes: test grants worth nothing. They never touch a person's real Vibes or a project's real Vibes wallet, and a sandbox app never charges a production account. Use the sandbox to exercise the whole flow — create, pay sheet, webhook, refund — before your app is reviewed for production.

Refunds

POST /api/v1/vibes/charges/:id/refund returns a paid charge's Vibes to the person, from your project's Vibes wallet. The charge becomes refunded and the app gets the refunded webhook. A charge can be refunded once; a repeat is answered from the charge's status rather than moving the Vibes again. Only the app that created the charge may refund it.

Payouts

The Vibes your app receives sit in your project's Vibes wallet. The project's owner cashes them out under the platform's payout rules — the same rules a creator's payable balance uses: a minimum, a hold, and a payout provider. They are never converted into Balance. The current thresholds and hold are on the Billing policy page, which renders Billing's own policy, not a copy.

Limits and errors

The owner sets Vibes Pay's limits in OpenVibe's AI settings, namespace billing.vibes: the largest single charge, a person's daily Vibes spend in apps, and whether Vibes Pay is on. Sandbox is separate.

StatusMeans
401No app token, or it expired. Mint one with billing.vibes.charge.
402The person is short of Vibes; show Get Vibes on the sheet and let them top up.
403Vibes Pay is off for apps, or this app does not hold the capability.
404No such charge for this app.
409The reference was reused with a different request; never retry it as a new charge.
429Too many requests; retry after the header says.