Transactions
This guide covers integrating Transactions: reporting purchases so a parent can see them, asking a parent to approve a purchase before you charge, and letting a completed card payment stand in for parental consent.
For what transactions are and where the boundary between k-ID and your product sits, read Transactions concepts first. Payment-as-verification has its own concept page.
Before you start
- Check that Transactions are available to your organization. Please contact k-ID to have this capability enabled for your organization.
- Enable the parts you use. Transactions are off by default and configured per product in the Compliance Studio Transactions section, with Transactions the switch the other two depend on. Until the switch a given endpoint needs is on, that endpoint returns
FEATURE_DISABLED. See Feature flags for the three switches and what each gates. - Configure your webhook endpoint, if you use purchase approval. The approval outcome arrives as a webhook. Set the URL and secret under Developer Settings for the product, and validate signatures as described in the webhooks overview.
- Subscribe to the event you handle. An endpoint receives only the event types it's subscribed to, and k-ID drops the rest without a delivery attempt or an error. On the product's Developer Settings page, select
Transaction.PurchaseApprovalResultfor your endpoint.
Reporting purchases
/transaction/push records completed purchases against a session. Call it after a purchase completes, including each time a recurring one renews.
Each event carries its own id for idempotency, a title and optional description shown to the parent as-is, an amount in the currency's minor units, a currency, the timestamp it completed, an optional url where the parent can manage it, and a status.
| Field | Notes |
|---|---|
id | A UUID your product generates. Together with sessionId it's the idempotency key: resubmitting an accepted id counts as accepted again without recording twice. |
type | purchase. |
title, description | Shown to the parent as-is. title is required; description is optional. |
amount | Integer in the currency's ISO 4217 minor units, so 499 is 4.99 in USD and 499 in JPY. Zero is allowed, for a free item. |
currency | ISO 4217 code. Uppercase is canonical; lowercase is accepted and normalized. |
timestamp | RFC 3339 UTC, within the last 7 days and not in the future. |
url | Optional HTTPS link the parent can open to manage the purchase, for example to cancel a subscription. |
status | successful, or failed for a charge that was attempted and didn't complete. Reporting failed lets a parent tell a purchase that went through from one that didn't. |
One call takes up to 100 events:
{
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"events": [
{
"id": "9d4e2f81-7a3b-4c56-8e90-1f2a3b4c5d6e",
"type": "purchase",
"title": "Starter Pack",
"description": "500 coins and a cosmetic item",
"amount": 499,
"currency": "USD",
"timestamp": "2026-06-24T09:00:00Z",
"status": "successful",
"url": "https://your-webstore.example.com/manage?ref=order-4455667"
}
]
}
Events are processed independently, so read the per-event statuses rather than assuming the whole batch succeeded:
{
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"accepted": 1,
"rejected": 0,
"events": [
{ "id": "9d4e2f81-7a3b-4c56-8e90-1f2a3b4c5d6e", "status": "accepted" }
]
}
A reason of invalid_input on a rejected event means it broke a validation rule and retrying it unchanged fails again. A reason of internal_error is transient and safe to retry. The whole batch is rejected, rather than per event, when the session doesn't exist or has been revoked.
Reported purchases surface to the linked parent as a per-child Purchases view in Family Connect, and the amounts also feed the activity a trusted adult reads there and in the recurring digest email.
Requesting approval
When you want a parent's decision before charging, /transaction/request-purchase opens an approval request. Ask before you charge, and complete the purchase only once an approved outcome arrives.
{
"sessionId": "b1a6482d-5242-4b4a-aa88-3fa52595a672",
"id": "9d4e2f81-7a3b-4c56-8e90-1f2a3b4c5d6e",
"title": "Starter Pack",
"description": "500 coins and a cosmetic item",
"amount": 499,
"currency": "USD"
}
The response carries the id you sent and an expiresAt. Store the id; it correlates the outcome webhook.
{
"id": "9d4e2f81-7a3b-4c56-8e90-1f2a3b4c5d6e",
"expiresAt": "2026-06-25T16:00:00Z"
}
k-ID emails the request to the parent on record for the session, who grants or denies it in Family Connect. The outcome arrives as a Transaction.PurchaseApprovalResult webhook carrying approved, denied, or expired, correlated by data.id.
Design for these properties of the flow:
- Approve before you charge. The request is a gate in front of the purchase, not a record of one. Charge on
approved; treatdeniedandexpiredas "don't charge." - A request expires after 24 hours. Expiry fires the webhook with
status: "expired"and is a normal outcome, not a failure. The player can be offered the purchase again. TreatexpiresAtas the deadline rather than waiting on the webhook indefinitely. - The request is idempotent per
id. Resubmitting anidthat's still awaiting a decision returns the pending request unchanged instead of opening a second one or prompting the parent twice. - A player holds at most 20 pending requests. Beyond that the call is rejected with
INVALID_INPUTuntil one resolves. - A player needs a linked parent to be prompted. The request is sent to the approver on record for the session. Opening one for a player whose parent isn't linked has no one to notify, so establish the parent link first. See Verified parent linking.
See Transaction.PurchaseApprovalResult for the payload and every status.
Payment-as-verification
Where the credit-card verification method is enabled for a session's jurisdiction, a completed credit-card payment can satisfy the parental-consent step instead of a separate verification. Read Payment-as-verification concepts for what qualifies and why; this section is the integration.
Payment-as-verification attaches to the standard parental-consent challenge. Where you would send the consent email with /challenge/send-email, send it with /challenge/send-email-with-payment-verification instead, adding the paymentVerification attestation. Everything else about the challenge is unchanged.
The attestation describes the payment in k-ID's fields, whichever provider you use:
| Field | Notes |
|---|---|
provider | The merchant of record that processed the payment. Scopes invoiceId. |
invoiceId | Your merchant of record's identifier for the payment. k-ID treats it as opaque. |
payerEmail | The email address that paid. |
instrument | What the payment was made with: card, wallet, or other. |
funding | The card's funding type: credit, debit, prepaid, or unknown. Only credit qualifies; send unknown when your provider doesn't report one. |
Send it on the challenge the way you send any other consent email, with the attestation in paymentVerification:
{
"challengeId": "ae6d4729-af32-42ea-8ef2-ff46c7664802",
"email": "parent@example.com",
"paymentVerification": { "...": "the normalized attestation from your merchant of record" }
}
k-ID neither processes nor witnesses the payment, so attest only to real, completed payments.
Normalizing a provider's payment
Merchants of record each report a payment differently. You can optionally use /transaction/lookup-provider to normalize the raw payment signal from your merchant of record into the standard attestation shape.
As an example, for Xsolla you send the raw payment webhook body, which must include the card BIN:
{
"provider": "xsolla",
"data": { "...": "the raw Xsolla payment webhook body" }
}
It returns the normalized attestation to drop into paymentVerification on the consent email:
{
"provider": "xsolla",
"invoiceId": "2110445753",
"payerEmail": "parent@example.com",
"instrument": "card",
"funding": "credit"
}
Feature flags
Transactions have three switches, and Transactions is the switch the other two require:
| Switch | What it enables | Endpoints it gates |
|---|---|---|
| Transactions | Reporting purchases and normalizing a provider's payment | /transaction/push, /transaction/lookup-provider |
| Purchase Controls | Asking a parent to approve a purchase | /transaction/request-purchase |
| Payment-as-verification | A qualifying card payment standing in for consent | /challenge/send-email-with-payment-verification |
An endpoint whose switch is off returns FEATURE_DISABLED. Purchase Controls and Payment-as-verification each also require Transactions, so a product with only Transactions on can report purchases and normalize payments but can't request approval, and a product with none can call none of them.
Prelaunch checklist
- Every
/transaction/pushreads the per-event statuses rather than assuming the batch succeeded, and retries onlyinternal_errorevents. - Purchases are reported with a real
status, includingfailedcharges, so a parent sees what actually happened. - Purchase approval asks before charging, and both
deniedandexpiredare handled as "don't charge." - The product's webhook is subscribed to
Transaction.PurchaseApprovalResult, and the handler is idempotent ondata.idand validates the signature. - A player has a linked parent before your product opens an approval request for them.