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.
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;
bookedAmountsays 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:
- Verify your provider's signature on the raw body.
- Report under your provider's own IDs, so a redelivered event records nothing new. Report a charge before the subscription state it changes.
- Map your provider's statuses onto the five subscription statuses explicitly; they rarely match one to one.
- 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.