API and integrations
Last updated October 27, 2026 · API version v1
Venda Wallet has a versioned JSON API, webhooks and a remote MCP server. Use them to build your own tools, or to connect another app to Venda Wallet for the people who use it. Base URL https://vendawallet.com/api/v1. Amounts are whole US cents; times are UTC ISO 8601. The full description is in openapi.json.
Who it's for
- Point of sale and bookkeeping apps for vendors. See sales, money added and refunds as they happen with webhooks, and record them in the vendor's own books.
- Market websites, maps and directories. Show which vendors at a market take Venda Wallet, straight from the public directory, with no key.
- Market organizers' own tools. Keep a market's listing and vendor list in step with your own system. Organizers manage listings only; they can't change balances or approve anything.
- AI assistants and personal scripts. Check balances, see Zelle payments to confirm, or refund, with a key you make yourself.
Public directory
Markets, events and vendor pages are public. Read them with no key, from a server or straight from a web page: these answers allow any website (Access-Control-Allow-Origin: *).
GET /directoryvendors in the directory. Query:event,category,q.GET /eventsmarkets and events.qsearches every US farmers market by name, city, state or ZIP.GET /events/{slug}andGET /vendors/{slug}one market or vendor.
curl "https://vendawallet.com/api/v1/events?q=94110"
Farmers market details come from Harvestly Markets' US Farmers Markets 2026, based on the USDA Local Food Portal, under CC BY 4.0. If you show them, credit the source and the license, like the markets page does. Please link each vendor back to its page on Venda Wallet. Vendors can't add themselves to the directory through the API.
API keys and scopes
Sign in, open Settings, then API keys and apps, and create a key. Send it as Authorization: Bearer vw_…. Each key reaches only the areas you pick, read only or read and write. Keys never expire; remove one in Settings to cut it off right away.
| Scope | Covers |
|---|---|
| account:read account:write | Your account: your name, notifications and settings. Write includes read. |
| wallet:read wallet:write | Your wallet: your cards, balances, receipts and gifts. Write includes read. |
| vendor:read vendor:write | Your stall: sales, customers, Zelle payments to confirm and refunds. Write includes read. |
| organizer:read organizer:write | Your markets: market listings and the vendors who sell there. Write includes read. |
| rent:read rent:write | Your rent receipts: properties, tenants and the rent receipts you issue. Write includes read. |
| webhooks:read webhooks:write | Webhooks: messages sent to another app when something happens. Write includes read. |
Paths under /me need account, /cards, /activity, /topups, /charge-requests, /ledger and /gifts need wallet, /vendor needs vendor, /organizer needs organizer and /webhooks needs webhooks. GET needs :read; anything else needs :write. Each operation in openapi.json lists its scope as x-required-scope. A key without it gets 403 missing_scope. Keys made before scopes existed keep working: read-only keys have every :read scope and read-and-write keys have all of them.
Write calls answer 403 consent_required until the key's owner has accepted the current terms in the app. Anything done with a key counts as done by its owner (see the terms).
curl https://vendawallet.com/api/v1/vendor/today \ -H "Authorization: Bearer vw_..."
Connect with Venda
For apps used by many people: instead of asking each person to copy a key, send them to Venda Wallet to allow your app. It's the standard OAuth 2.0 authorization code flow with PKCE (S256). There's nothing to register; you name your app in the request.
1. Send the person to Venda Wallet
https://vendawallet.com/app/connect ?client_name=Market+Books &redirect_uri=https://marketbooks.example/venda/callback &scope=vendor:read+webhooks:write &state=RANDOM_STATE &code_challenge=BASE64URL_SHA256_OF_VERIFIER &code_challenge_method=S256
They sign in if needed, see your app's name, the site they'll go back to and what it can do, and tap Allow or Cancel. redirect_uri must be https (or http://localhost while you build).
2. Get the code back
Allow sends them to redirect_uri?code=…&state=…. Cancel sends error=access_denied. Check that state matches what you sent. A code works once, within 10 minutes.
3. Swap the code for a key, from your server
curl https://vendawallet.com/api/v1/connect/token \
-d grant_type=authorization_code \
-d code=CODE \
-d redirect_uri=https://marketbooks.example/venda/callback \
-d code_verifier=VERIFIER
{"access_token":"vw_...","token_type":"Bearer","scope":"vendor:read webhooks:write","key_id":"key_..."}The access token is an API key with just those scopes. Keep it on your server, never in a browser or app bundle. It lasts until the person removes your app in Settings, where it shows as a connected app with your site's name. Then calls answer 401, and you should ask them to connect again.
Webhooks
Webhooks tell your server when something happens at the caller's stall, so you don't have to keep asking. Add up to 5 per account with POST /webhooks (needs webhooks:write). The answer includes the webhook's signing secret, shown this once.
curl https://vendawallet.com/api/v1/webhooks \
-H "Authorization: Bearer vw_..." -H "Content-Type: application/json" \
-d '{"url":"https://marketbooks.example/hooks/venda","events":["payment.created","refund.created"]}'| Event | When |
|---|---|
| topup.claimed | A buyer says they sent you money by Zelle. Check your bank, then confirm it. |
| topup.reminded | A buyer asks you to check again after you said Not yet. |
| topup.confirmed | Money added was confirmed and is on the buyer's card. |
| payment.created | A buyer paid you from their card (scanned, approved or from the directory). |
| charge_request.updated | A charge waiting for the buyer was approved or declined. |
| refund.created | You refunded a payment back to a buyer's card. |
| cash_out.created | You took money off a card after sending it back by Zelle. |
Leave out events, or send ["*"], for all of them. POST /webhooks/{id}/test sends a ping, and GET /webhooks/{id}/deliveries shows the last 50 and how they went.
What arrives
POST /hooks/venda
Venda-Event: payment.created
Venda-Delivery: whd_...
Venda-Signature: t=1793125200,v1=5f1c...
{"id":"whd_...","type":"payment.created","created_at":"2026-10-27T16:20:00.000Z","api_version":"v1",
"data":{"id":4182,"card_id":"c_...","vendor_id":"v_...","type":"charge","amount":1250,"via":"scan","at":"..."}}Check the signature
Compute HMAC-SHA256 of ${t}.${raw body} with your secret and compare it with v1. Refuse messages more than 5 minutes old, and use id to skip any you've already handled.
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const want = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
return fresh && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(v1));
}Answers and retries
Answer with any 2xx within 10 seconds. Anything else is retried with growing gaps for about two days. Redirects aren't followed. A webhook that keeps failing for three days is turned off; turn it back on with PATCH /webhooks/{id} {"enabled": true}. Webhook addresses must be public https addresses.
Safe retries
Send an Idempotency-Key header (any unique string up to 255 characters) with a POST, PATCH, PUT or DELETE. If the connection drops, send the same request with the same key: you get the first answer back, marked Idempotent-Replayed: true, instead of a second charge or refund. Keys are kept for 24 hours. The same key on a different request answers 422 idempotency_mismatch.
Limits and errors
- API keys: 120 requests a minute per account. Over that,
429 rate_limitedwithRetry-After. - Signed-out reads of the public directory: limited per IP address, generously.
- The app's own limits still apply, such as how much a card can hold and daily caps.
Errors look like this, with a plain-language message you can show people:
{"error":{"code":"missing_scope","message":"This API key doesn't have the vendor:write scope."}}v1 grows without breaking: new fields and events can appear, so ignore ones you don't know. A change that would break an integration goes into a new version.
MCP and AI agents
Remote MCP server (Streamable HTTP): https://vendawallet.com/mcp, with an API key as a bearer token. It lists only the tools the key's scopes allow. Receipts (rent scope): receipts_overview, list_receipts, get_receipt, issue_receipt, quick_sale_receipt and send_receipt. Prepaid cards: search_directory, list_my_cards, get_card, claim_topup, vendor_today, vendor_money_in, vendor_confirm_topup, vendor_mark_not_yet, vendor_customers, vendor_customer and vendor_refund. A plain-text summary for agents lives at /llms.txt.
Rules for integrations
- Venda Wallet never holds or moves money. Buyers pay vendors by Zelle® from their own bank, and Venda Wallet keeps the tally.
- Never report a Zelle payment that wasn't sent, and never confirm money added without the vendor checking their bank.
- Ask only for the scopes you need, and keep keys and webhook secrets on your server.
- Problems between a buyer and a vendor are between them. Your app can't settle or reverse anything for them.
- Don't present your app as made or endorsed by Venda Wallet, a bank or Zelle. Venda Wallet is not authorized, endorsed or sponsored by Early Warning Services, LLC, owner of the Zelle® marks.
- Use buyers' and vendors' details only to do what they connected your app for. Don't sell them.
Building something? Tell us at hello@vendawallet.com.