Quickstart: your first sale
Create a product, an offer and a checkout, embed it on a page, and receive the payment_success webhook.
This page takes you from an empty workspace to a working checkout and a webhook landing on your server. Four API calls, a script tag, and about ten minutes.
Everything here uses the v0 REST API. You can do all of it in the dashboard instead — the API is shown because it is what you will automate later.
There is no separate test mode for payments today. A purchase made through the checkout you build here is a real one. You can exercise your webhook without paying — see step 6.
Before you start
You need a Fungies workspace and your store's address, which looks like
https://yourstore.fungies.io. Both come from the dashboard.
Every example below sets two headers. Reads need only the public key; writes — everything on this page — need both.
x-fngs-public-key: pub_your_public_key
x-fngs-secret-key: sec_your_secret_keyStep 1: Get your API keys
Create a key pair in the dashboard under Developers → API keys. This is the one step with no endpoint: keys cannot be created over the API.
Keep the secret key server-side. It authorises writes; a public key alone never does. See Authentication for the full model.
Step 2: Create a product
A product is the thing you sell. It carries the name, description and media — not the price.
curl -X POST "https://api.fungies.io/v0/products/create" \
-H "x-fngs-public-key: pub_your_public_key" \
-H "x-fngs-secret-key: sec_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Starlight Soundtrack",
"type": "DigitalDownload"
}'name and type are the only required fields. The other product types are listed on the
Product object page; VirtualItem and VirtualCurrency additionally
require a projectId.
{
"status": "success",
"data": {
"product": {
"object": "product",
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Starlight Soundtrack",
"type": "DigitalDownload",
"status": "ACTIVE"
}
}
}Keep data.product.id. The next step needs it.
Step 3: Create an offer
An offer is a buyable price for a product. One product can carry several — per region, per billing interval, per variant.
curl -X POST "https://api.fungies.io/v0/offers/create" \
-H "x-fngs-public-key: pub_your_public_key" \
-H "x-fngs-secret-key: sec_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"productId": "550e8400-e29b-41d4-a716-446655440000",
"name": "Starlight Soundtrack",
"currency": "USD",
"price": 29.99,
"limit": null
}'Four fields are required. Two of them catch people out.
| Field | Note |
|---|---|
productId | The product from step 2. |
currency | Your workspace holds one currency, seeded by the first offer or product you create. A later mismatch is rejected with 400 OFFER_CURRENCY_MISMATCH rather than silently converted. |
price | A decimal amount in that currency — 29.99 means $29.99. It must be above zero; for a free offer send "freeProduct": true instead, which forces the price to 0. |
limit | Required, and nullable. null means unlimited stock; a number caps how many can be sold. It reads like an optional field and is the most common first-request 400. |
price is asymmetric. You send a decimal (29.99) and every response returns the smallest
currency unit (2999). Currencies with no decimal places, such as JPY, read the same in both
directions. Divide before you display, or your prices will be a hundred times too large.
To sell a subscription instead, add recurringInterval (day, week, month or year) and
recurringIntervalCount. Leaving them out makes it a one-time purchase.
{
"status": "success",
"data": {
"offer": {
"object": "offer",
"id": "660e8400-e29b-41d4-a716-446655440001",
"price": 2999,
"currency": "USD",
"status": "OPEN"
}
}
}New offers are created OPEN. There is no publish step.
Step 4: Create a checkout element
A checkout element is the buyable page you embed. It wraps one or more offers.
curl -X POST "https://api.fungies.io/v0/elements/checkout/create" \
-H "x-fngs-public-key: pub_your_public_key" \
-H "x-fngs-secret-key: sec_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Soundtrack checkout",
"offersIds": ["660e8400-e29b-41d4-a716-446655440001"]
}'offersIds is the only required field and needs at least one entry — note the spelling. A
subscription offer cannot be combined with other offers in the same element. name is optional —
if you omit it, one is generated for you (Checkout with <offer name>, sometimes with extra
details like the billing interval or region appended, or Checkout with N offers for multiple),
so set your own for a more meaningful label.
{
"status": "success",
"data": {
"checkoutElement": {
"object": "checkoutElement",
"id": "770e8400-e29b-41d4-a716-446655440002",
"name": "Soundtrack checkout",
"status": "ACTIVE",
"subscriptionOffer": false,
"offers": [{ "object": "offer", "id": "660e8400-e29b-41d4-a716-446655440001" }]
}
}
}The response gives you an id, not a URL. You build the URL yourself from your store address:
https://yourstore.fungies.io/checkout-element/770e8400-e29b-41d4-a716-446655440002Step 5: Embed it
Add your website to Authorized Domains
first. The checkout sets a frame-ancestors policy from that list. On a domain that isn't on
it the browser refuses the frame, and your page shows an empty box with no JavaScript error to
catch. This is the single most common reason a first embed "does nothing".
The smallest working integration is a button and a script tag. No build step, no keys on the page.
<button
data-fungies-checkout-url="https://yourstore.fungies.io/checkout-element/770e8400-e29b-41d4-a716-446655440002"
data-fungies-mode="overlay">
Buy the soundtrack
</button>
<script
src="https://cdn.jsdelivr.net/npm/@fungies/fungies-js@0"
defer
data-auto-init>
</script>data-auto-init tells the SDK to initialise itself and wire up every element carrying
data-fungies-checkout-url. Use data-fungies-mode="embed" with a
data-fungies-frame-target instead if you want the checkout inline rather than in an overlay.
Pin the major version — @fungies/fungies-js@0, as above. @latest would pull a future major
into a page you are not watching. Full option list:
SDK reference.
Open the page and click the button. The checkout should load.
Step 6: Receive the payment_success webhook
A completed checkout is not a fulfilled order until your server knows about it. Register an
endpoint and subscribe it to payment_success.
curl -X POST "https://api.fungies.io/v0/webhooks/create" \
-H "x-fngs-public-key: pub_your_public_key" \
-H "x-fngs-secret-key: sec_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.yourgame.com/fungies/webhook",
"status": "active",
"secret": "a-long-random-string-you-generate",
"events": ["payment_success"]
}'| Field | Note |
|---|---|
url | Must be a publicly reachable http:// or https:// address. localhost will not work — use a tunnel while developing. If a URL is rejected, the error says why. |
status | active or inactive. An inactive webhook is configured but receives nothing. |
secret | You choose it. Any string of 16 characters or more. It signs every delivery. |
events | Omit it and the webhook is subscribed to nothing, so it receives nothing. |
Each delivery arrives as a POST with an x-fngs-signature header — sha256_ followed by the
HMAC-SHA256 of the raw request body, keyed with your secret. Verify it against the raw bytes,
before any JSON parsing. There is a working Express example on
Webhook setup.
The payload looks like this:
{
"id": "880e8400-e29b-41d4-a716-446655440003",
"type": "payment_success",
"idempotencyKey": "880e8400-e29b-41d4-a716-446655440003",
"testMode": false,
"data": {
"order": { "object": "order", "id": "990e8400-e29b-41d4-a716-446655440004" },
"payment": { "object": "payment" },
"user": { "object": "user" },
"customer": { "object": "user" },
"items": []
}
}customer is the same object as user, kept for older integrations. Which keys appear under
data depends on the event — subscription events carry different ones. The matrix is on the
Event object page.
Grant access on payment_success, and only on payment_success. Delivery is at-least-once and
unordered, so deduplicate on idempotencyKey and never downgrade state because an older event
arrived late. Both traps are explained on Webhooks overview.
Test it without buying anything
POST /v0/webhooks/sendTestEvent fires a synthetic event of the type you name at every active
webhook subscribed to it, retries included:
curl -X POST "https://api.fungies.io/v0/webhooks/sendTestEvent" \
-H "x-fngs-public-key: pub_your_public_key" \
-H "x-fngs-secret-key: sec_your_secret_key" \
-H "Content-Type: application/json" \
-d '{ "eventType": "payment_success" }'If nothing arrives, GET /v0/webhooks/{webhookId}/attempts shows what we tried and what your
endpoint answered.
What you have now
A product, a priced offer, an embeddable checkout, and a server that hears about payments. That is a complete integration — everything else is refinement.
Last updated