VoidhashDocs
DevelopersHelp centerGuidesAPI reference
Dashboard
Get started
Introduction
Quickstart
How Voidhash works
Glossary
Versions and releases
Concepts
Teams, projects and environments
API keys
Products and perks
Offers
Subscription lifecycle
People and identity
Events
Features
Paywalls in your app
Feature flags and A/B tests
Paywall components
Push notifications
Ad attribution in your app
Client SDKs
React Native
Swift
Kotlin
Web
Your backend
Node.js
PHP
Go
Rust
Other languages
Payment sources
Forward store notifications
Developer tools
CLI
MCP server
Query analytics with VoidQL
Payment sources
Transaction fields
Report subscriptions
Build a provider endpoint

Payment sources

Record purchases you take outside the stores and Stripe, so they grant access and count as revenue.

A payment source records payments you take outside the App Store, Google Play and Stripe: through another payment provider, such as Paddle or Lemon Squeezy, by bank transfer, or with vouchers. Your server reports each payment, refund and subscription change, and Voidhash records it like a store purchase: it grants the product's perks, counts in revenue analytics, and sends your webhooks. Voidhash can't check these payments with anyone, so it marks them verified: false.

How it works

Only the first direction is required:

  • Report (Payments API). Your server sends each payment and subscription state to Voidhash, usually from your provider's webhooks.
  • Provider endpoint (optional). Voidhash calls an endpoint on your server to import products and history, check on subscriptions it hasn't heard from, and cancel or refund at your provider.
Your providerYour serverVoidhashwebhookPayments API reportsigned endpoint callprovider API call

Solid: reporting payments (required). Dashed: the provider endpoint (optional).

The server libraries for Node.js, PHP, Go and Rust implement both directions. This page uses plain HTTP.

Set up a source

Add the source

In Studio, open Settings → Payment providers and choose Add payment source. Give it a Name and a Slug, such as bank-transfer, which names the source in webhooks and analytics and can't be changed. Add one source per place you take payments.

Map your product keys

A source names what it sells with its own product keys, such as LIFETIME or a price ID. Open each Voidhash product and add the key under the source, or use Import products once a provider endpoint is connected. Map as many keys to a product as you sell it under.

A mapping covers All environments by default. When your test system uses different keys, map the live key as Production only and the test key as Test only. A payment for an unmapped key waits, and is recorded once you map it.

Create a secret key

Report with a project secret key, ideally with only Purchases write access (project:purchases:write). A production key (vh_sk_…) records production payments, a test key (vh_sk_test_…) test payments.

The source's Payments API tab shows the Transactions endpoint and Subscriptions endpoint, with your project ID and the source's payment provider ID filled in. The libraries take that payment provider ID, not the slug.

Report a payment

Send each payment once it has settled, with PUT and under an ID that's unique within the source, such as your provider's transaction ID.

curl -X PUT "https://api.voidhash.com/api/v1/projects/$VOIDHASH_PROJECT_ID/payment-providers/$PAYMENT_SOURCE_ID/transactions/tr_10042" \
  -H "Authorization: Bearer $VOIDHASH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "charge",
    "distinctId": "user_123",
    "productKey": "LIFETIME",
    "amount": 4900,
    "currency": "EUR",
    "tax": 818,
    "occurredAt": "2026-10-20T09:30:00Z"
  }'

You can also record one by hand with Record payment on the source's page, or with voidhash provider record-payment.

Transaction fields describes every field of the request and the response.

A charge without subscriptionId completes a one-time purchase. The response is the recorded transaction, with status applied, or pending while it waits for a product mapping or for the charge it refers to.

A pending payment is recorded on its own once what it waits for arrives; don't send it again. If it's invalid by then, for example a refund in another currency than its charge, it's dropped and you can send it again under the same ID.

Retries and corrections

The same ID with the same body changes nothing and returns the same result, so resend after a timeout and let your provider redeliver its events. The same ID with a different body fails with 409 (transaction_conflict), because a recorded payment is never edited. To correct one, refund it and report the right payment under a new ID.

A rejected request records nothing and doesn't use up its ID. Transaction fields lists the other errors these endpoints return.

Refunds

Report a refund your provider made under its own ID, with refundsTransactionId naming the charge. It takes its person and product from the charge, and its currency must match.

  • A refund of everything left of the charge ends the access it granted.
  • A smaller refund reduces revenue and keeps access. The refund that returns the rest ends it.
  • A refund can't return more than is left. When refunds race, the later one records only what's left; bookedAmount says how much.
  • A charge of 0, such as a free voucher, is ended with a refund of 0.

Customers without a distinct ID

When you don't know the buyer's distinct ID, send customer.externalId instead. Voidhash records the purchase on a stand-in person, and later payments with the same customer.externalId go to the same person. Once you know who they are, send both fields with the next payment: Voidhash merges the stand-in into that person, purchases and access included.

Sell on your website

To credit a website purchase to the visitor who bought, read their distinct ID with the web SDK's getDistinctId() and send it to your server. Store it with the checkout you create at your provider, for example in its custom data, and report the payment with it. See Sell on the web.

Subscriptions

A subscription's payments are charges with subscriptionId, periodStart and periodEnd, and its state, which decides access, goes to a separate endpoint. See Report subscriptions.

Translate your provider's webhooks

Most integrations report from your provider's webhooks. For each event:

  1. Verify your provider's signature on the raw body.
  2. Report under your provider's own IDs, so a redelivered event records nothing new. Report a charge before the subscription state it changes.
  3. Map your provider's statuses onto the five subscription statuses explicitly; they rarely match one to one.
  4. Answer with an error status when a report fails, so your provider redelivers. Acknowledge a transaction_conflict, since redelivery can't fix it, and look into it.

Connect an endpoint

Build the optional provider endpoint with a server library's provider handler, then enter its URL on the source's Provider endpoint tab.

Test and staging

Report from staging into the same project with a test secret key (vh_sk_test_…), so everything it reports is recorded as test. Connect staging as the source's test endpoint, so imports, cancellations and refunds of test records go there rather than to production. Use your provider's sandbox, and the test publishable key on your staging website. Test payments stay out of production revenue, never unlock access in production apps and reach only test webhooks. In Studio, choose Test in the project switcher to see them; see Environments.

In webhooks and analytics

Purchases from a source send the usual purchase webhooks with provider: "external", source set to the source's slug, and verified: false; store purchases carry verified: true. Revenue events carry the same fields, so you can break revenue down by source. See Webhook events.

Next steps

  • Report payments with Node.js, PHP, Go or Rust
  • Report subscriptions
  • Build a provider endpoint
  • Map store products
  • Webhook delivery

Something wrong or missing on this page? Tell us by email or on Discord.

PreviousReceive webhooksNextTransaction fields

On this page

How it worksSet up a sourceAdd the sourceMap your product keysCreate a secret keyReport a paymentRetries and correctionsRefundsCustomers without a distinct IDSell on your websiteSubscriptionsTranslate your provider's webhooksConnect an endpointTest and stagingIn webhooks and analyticsNext steps