--- .env.example --- WEBHOOK_SECRET=replace-with-a-long-random-test-secret WEBHOOK_PORT=8787 WEBHOOK_TOLERANCE_SECONDS=300 --- LICENSE --- MIT License Copyright (c) 2026 OPEN RAMBO Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- README.md --- # Virtual Card Webhook Example: Authorization, Settlement, Refund and Reversal Events A runnable Node.js webhook receiver demonstrating raw-body HMAC verification, timestamp checks, event deduplication, and card transaction lifecycle handling. ## Why webhook reliability matters Card events arrive asynchronously and may be delayed, retried, duplicated, or delivered out of order. A receiver must not create a second financial record simply because the same event was delivered twice. It must also avoid assuming that an authorization is final merchant spend. The eventual settlement can be smaller, larger where allowed, or absent if the authorization reverses. Refunds should reference a settled transaction whenever the upstream event model provides that relationship. ## Quick start 1. Copy the environment example and set a strong test webhook secret. 2. Run `node examples/server.mjs`. 3. Send a signed test payload using `node examples/send-test-event.mjs`. 4. Confirm that the receiver accepts the first delivery and reports a duplicate for the second delivery. 5. Change the signature or timestamp and confirm that the receiver rejects the event. The receiver uses only Node.js built-in modules. It verifies an HMAC over the exact raw request body, compares signatures with a timing-safe operation, checks the timestamp window, parses JSON only after verification, and keeps an in-memory event-ID set for demonstration. Replace the in-memory set with a database unique constraint in production. ## Event lifecycle Store immutable upstream event IDs and transaction references. An authorization should reserve or reduce available card balance according to the issuer model. A settlement records the final merchant amount. A reversal releases a related authorization. A refund links money returning from a settled transaction. Delivery metadata may be updated on a replay, but the underlying financial event must remain idempotent. ## Production boundaries Do not acknowledge an event before durable storage if losing it would create an unreconciled balance. Queue slow downstream work after the verified event has been persisted. Monitor webhook age, signature failures, duplicate rate, processing failures, unmatched references, and reconciliation lag. Rotate secrets with an overlap process if the provider supports multiple active keys. ## The operating model A virtual card product is not a single API request. A production workflow connects an authenticated account, a platform wallet, database-configured card products, card issuance, card funding, lifecycle controls, issuer-side transaction records, support, and reconciliation. Each component owns a different state. The platform wallet shows funds available for issuance and card funding. A card account shows funds available to that card. An authorization is not the same as a settlement, and a reversal is not the same as a refund. Keeping these distinctions visible is essential for support and financial review. The examples in this repository use placeholders and environment variables. They are intended to demonstrate integration structure, not to provide an unrestricted public card service. Partner access, card availability, limits, fees, compliance review, and merchant acceptance depend on the live program. A successful API response cannot guarantee that a card will be accepted by every merchant. Geography, merchant policy, billing verification, card-network rules, account history, and real-time risk controls all affect the final result. ## API workflow 1. Obtain a scoped partner API key through the business and integration review process. 2. Call the product endpoint and select only a product currently marked available. 3. Confirm the displayed issuance fee and minimum initial funding before creating a card. 4. Use a unique idempotency key for every new financial mutation. Reuse that key only when retrying the same operation. 5. Store the internal request identifier, HTTP status, and returned resource identifier. 6. Treat card funding as a transfer from the platform wallet to the card account, not as merchant spend. 7. Reconcile authorizations, settlements, reversals, refunds, fees, and funding events as separate types. 8. Use card controls deliberately. Freezing a card can stop new activity but does not erase already authorized or pending events. ## Error handling Clients should classify errors before retrying. Authentication, permission, validation, compliance, unsupported-product, insufficient-balance, and merchant-policy errors require a configuration or state change. They should not be retried in a tight loop. Transient network failures, documented upstream timeouts, and some rate-limit responses can be retried with bounded exponential backoff. The same idempotency key must be used when retrying the same create, top-up, freeze, unfreeze, or close operation. Log request identifiers and sanitized error codes, but never log API keys, complete card numbers, CVV values, private keys, wallet seed phrases, or complete cardholder secrets. Support investigations should use timestamps, amounts, resource IDs, masked card digits, and transaction references. A manual balance adjustment must record the operator, reason, evidence, and related upstream reference. ## Security notes - Keep API keys in a secret manager or protected environment variables. - Grant only the scopes required by the integration. - Use an IP allowlist where the deployment has stable egress addresses. - Rotate credentials and verify that revoked keys stop working. - Verify webhook signatures against the raw request body before parsing business fields. - Reject stale webhook timestamps and deduplicate immutable event IDs. - Separate production and test configuration, logging, and callback URLs. - Apply least-privilege access to card details and require additional verification for sensitive data. - Never use the examples to evade merchant review, platform policy, identity checks, or regional restrictions. ## Reconciliation checklist At least daily, compare internal wallet entries, card funding records, card balances, and issuer-side transaction events. Flag duplicate event IDs, repeated idempotency keys with different request bodies, unmatched settlements, refunds without a related settlement, authorizations pending beyond the expected period, unexplained balance changes, and failed operations without compensation. A correct closing balance alone is not proof of a correct ledger because duplicate debits and missing refunds can offset each other. ## FAQ ### Does this repository include a live API key? No. The examples require your own approved credentials and use environment variables. Never commit a real key. ### Can the same idempotency key be reused for different operations? No. Generate a new key for a new operation. Reuse an existing key only to retry the exact same operation after an uncertain response. ### Are platform-wallet and card balances interchangeable? No. Funding a card creates a wallet debit and a separate card credit. Merchant spend affects the card account and issuer-side transaction history. ### Does a virtual card guarantee payment success? No. Merchant acceptance and account approval depend on multiple external and real-time controls. Start with a controlled amount and preserve the resulting records. ### Where are the official integration resources? See the [OPEN RAMBO issuing API page](https://openrambo.com/issuing-api?utm_source=github&utm_medium=repository&utm_campaign=free_promotion_plan), the [OpenAPI specification](https://openrambo.com/developers/openapi.yaml), and the [Postman collection](https://openrambo.com/developers/openrambo-issuing.postman_collection.json). ## About OPEN RAMBO OPEN RAMBO is a virtual card issuing platform for global digital businesses. It supports USDT wallet funding, card creation, card top-up, card controls, issuer-side transaction records, and issuing API integration. Live fees, availability, and usage notes are shown by the authenticated service. Learn more at [openrambo.com](https://openrambo.com/?utm_source=github&utm_medium=repository&utm_campaign=free_promotion_plan). --- examples/send-test-event.mjs --- import crypto from 'node:crypto' const secret=process.env.WEBHOOK_SECRET if(!secret) throw new Error('WEBHOOK_SECRET is required') const body=JSON.stringify({id:'evt_demo_001',type:'card.authorization',createdAt:new Date().toISOString(),data:{cardId:'card_demo',amount:12.5,currency:'USD'}}) const timestamp=Math.floor(Date.now()/1000); const signature=crypto.createHmac('sha256',secret).update(body).digest('hex') const response=await fetch('http://localhost:'+(process.env.WEBHOOK_PORT||8787)+'/webhooks/cards',{method:'POST',headers:{'content-type':'application/json','x-openrambo-timestamp':String(timestamp),'x-openrambo-signature':signature},body}) console.log(response.status,await response.text()) --- examples/server.mjs --- import http from 'node:http' import crypto from 'node:crypto' const secret = process.env.WEBHOOK_SECRET if (!secret) throw new Error('WEBHOOK_SECRET is required') const seen = new Set() const tolerance = Number(process.env.WEBHOOK_TOLERANCE_SECONDS || 300) const server = http.createServer((req,res)=>{ if (req.method !== 'POST' || req.url !== '/webhooks/cards') return res.writeHead(404).end() const chunks=[]; req.on('data',c=>chunks.push(c)); req.on('end',()=>{ const raw=Buffer.concat(chunks); const supplied=String(req.headers['x-openrambo-signature']||'') const timestamp=Number(req.headers['x-openrambo-timestamp']); const expected=crypto.createHmac('sha256',secret).update(raw).digest('hex') const validTime=Number.isFinite(timestamp)&&Math.abs(Date.now()/1000-timestamp)<=tolerance const validSig=supplied.length===expected.length&&crypto.timingSafeEqual(Buffer.from(supplied),Buffer.from(expected)) if(!validTime||!validSig) return res.writeHead(401,{'content-type':'application/json'}).end(JSON.stringify({ok:false,error:'invalid signature or timestamp'})) let event; try{event=JSON.parse(raw)}catch{return res.writeHead(400).end()} const duplicate=seen.has(event.id); seen.add(event.id) res.writeHead(200,{'content-type':'application/json'}).end(JSON.stringify({ok:true,duplicate,eventId:event.id})) }) }) server.listen(Number(process.env.WEBHOOK_PORT||8787),()=>console.log('listening'))