Server-to-Server Webhooks
This guide describes how to add and configure custom server-to-server webhook.
Keys live at the organization levelS2S API keys for webhooks are managed at the organization level — see Organization settings → S2S API Keys.
Server-to-Server (S2S) Webhooks push every Apphud subscription event to your own backend in real time as a POST request. Build your own logic, analytics, billing automation, or CRM sync without polling the Apphud API — your server reacts to trials, renewals, refunds, and other lifecycle events the moment they happen.
Webhooks are sent as a POST request to the URL you specify. You can use this data to implement custom logic or analytics on your own server.
How to Add Webhook?
Step 1 - Preparations on your server
Set up an HTTPS endpoint on your server that accepts POST requests with a JSON body. This is the URL Apphud will deliver webhooks to.
(Optional) Decide on a Secret token to validate incoming payloads. If you set one, Apphud will include it in the X-Apphud-Token HTTP header on every request.
Step 2 - Authentication
In Apphud, open Connections → Integrations, find Server-to-Server webhooks (on the All or Data Platforms tabs), and click Add connection. Choose a Source.
Enter the webhook name and specify the Webhook URL. Events will be sent to this URL using POST requests.
Step 3 - Security configuration
Optionally specify a Secret token that will be sent with each webhook request in the X-Apphud-Token HTTP header. Use this token to validate received payloads on your side.
Step 4 - Environment configuration
Choose whether to send Production, Sandbox, or both via this webhook.
Step 5 - Set up Filters (optional)
Navigate to Filters tab. Set up filters if needed as described on Integrations overview → Filters tab.
Step 6 - Set up Events list
Navigate to Events tab. Enable events you want to receive webhooks for.
Step 7 - Test connection (optional)
Save connection and test it. See How to Test Webhook below for end-to-end testing with services like RequestBin.
Step 8 - Enable and save
After successful testing, Enable connection and Save it.
Important NoteApphud sends only one request per webhook without retries. Your endpoint must be reliably reachable and idempotent.
Webhook Structure
The POST body for all webhooks follows the same structure:
{
"app": {},
"event": {},
"user": {}
}App
This dictionary contains information about the app.
| Field | Type | Description |
|---|---|---|
app.uid | String | Apphud app ID |
app.bundle_id | String | iOS Bundle ID |
app.package_name | String | Android Package Name |
Event
This dictionary contains information about the event.
| Field | Type | Description | Possible values |
|---|---|---|---|
event.id | String | Apphud event ID | |
event.created_at | Date | Date when this event was created by Apphud. | |
event.name | String | Event name | e.g. "trial_started" |
event.properties | Dictionary | Event properties. See below. | |
event.receipt | Dictionary | Apphud receipt information related to the event. See below. | |
event.store | String | Purchase store type |
event.properties
event.properties| Field | Type | Description | Possible values |
|---|---|---|---|
event.properties.product_id | String | Product ID | |
event.properties.reason | String | Depending on the event: reason of an expiration, refund reason, or product_id for billing_issue_resolved events | user_canceled, billing_issue, declined_price_increase, unavailable_product, unknown_error, app_issue, another_reason, product_id |
event.properties.currency | String | Local currency ISO code | |
event.properties.usd_price | Float | Price in USD | |
event.properties.local_price | Float | Price in local currency | |
event.properties.usd_tax | Float | Tax amount in USD | |
event.properties.local_tax | Float | Tax amount in local currency | |
event.properties.usd_proceeds | Float | Developer proceeds in USD after Apple/Google commission and taxes, if applicable | |
event.properties.local_proceeds | Float | Developer proceeds in local currency after Apple/Google commission and taxes, if applicable | |
event.properties.offer_type | String | Introductory or promotional offer payment mode (iOS only) | pay_up_front, pay_as_you_go, trial |
event.properties.offer_id | String | Promotional offer ID | |
event.properties.unit | String | The increment of time a subscription period is specified in (iOS only) | day, week, month, year |
event.properties.units_count | Integer | Number of units per subscription period (iOS only) | |
event.properties.family_shared | Boolean | Whether the purchase was used via Family Sharing | |
event.properties.product_bundle_id | String | Internal purchase identifier | |
event.properties.paywall_id | String | Internal Apphud paywall ID | |
event.properties.placement_id | String | Internal Apphud placement ID | |
event.properties.paywall_name | String | Paywall name as configured in Apphud | |
event.properties.paywall_identifier | String | Paywall identifier as configured in the app / SDK |
event.receipt
event.receipt| Field | Type | Description |
|---|---|---|
event.receipt.id | String | Apphud receipt ID |
event.receipt.product_id | String | Product ID |
event.receipt.purchased_at | Date | Transaction date, example: 2022-03-12T09:38:38.081Z |
event.receipt.intro_period | Boolean | Whether the subscription is in the introductory period |
event.receipt.trial_period | Boolean | Whether the subscription is in the trial period |
event.receipt.transaction_id | String | App Store transaction ID / Android Order ID |
event.receipt.original_transaction_id | String | App Store original transaction ID / Android Purchase Token |
event.receipt.price_usd | Float | Price in USD |
event.receipt.proceeds_usd | Float | Revenue in USD after Apple / Google commission deduction |
event.receipt.price | Float | Price in local currency |
event.receipt.proceeds | Float | Revenue in local currency after Apple / Google commission deduction |
event.receipt.currency | String | Local currency ISO code, example: USD |
event.receipt.quantity | Integer | Always 1 |
event.receipt.apple_share | Float | Store commission. Can be 0.15 or 0.3. |
User
This dictionary contains information about the user.
| Field | Type | Description |
|---|---|---|
user.created_at | String | User created date, example: 2022-03-12T09:38:38.081Z |
user.user_id | String | User ID |
user.total_spent | Float | User total spent in USD |
user.payments_count | Integer | User payments count |
user.ip | String | User IP address |
user.uid | String | Apphud user ID (browser user id) |
user.idfv | String | Device IDFV on iOS, App Set ID on Android |
user.country_iso_code | String | User country code, example: US |
user.time_zone | String | User's current timezone, example: Asia/Jakarta |
user.language | String | User's current language, example: en |
user.internal_attribution | Object | If available, captures data from Web-to-App flows, Apphud Flows, and Apple Search Ads Integration. Fields may include: status, provider, ad_network, campaign, ad_set, keyword, custom1 (Click/Impression for ASA). |
user.partner_attribution | Object | Partner attribution data, if available. May include attribution provider and campaign metadata such as campaign, ad_network, ad_set, keyword, status. |
user.variations[] | Array of Objects | List of Experiments (A/B tests) the user took part in. Each object may include experiment uid, name, variation_name, status, created_at, and launched_at. |
user.paying | Boolean | Whether the user is currently paying |
user.currency_country_code | String | Country code used for currency calculations (ISO 3166-1 alpha-2) |
user.email | String | Reserved User property for email assigned via Apphud SDK |
user.name | String | Reserved User property for name |
user.phone | String | Reserved User property for phone |
user.age | String | Reserved User property for age |
user.gender | String | Reserved User property for gender |
user.properties[] | Array | Custom user properties assigned via Apphud SDK |
user.subscriptions[] | Array | All subscriptions of the user. See below. |
user.devices[] | Array | All user devices. See below. |
user.subscriptions[]
user.subscriptions[]Each element of this array is a dictionary with the following structure:
| Field | Type | Description | Possible values |
|---|---|---|---|
user.subscriptions[].id | String | Apphud subscription ID | |
user.subscriptions[].product_id | String | Product ID | |
user.subscriptions[].group | String | Subscription group name | |
user.subscriptions[].status | String | Current subscription status | trial, intro, promo, regular, refunded, expired, promotional |
user.subscriptions[].intro_period | Boolean | Whether the subscription is currently in an introductory period | |
user.subscriptions[].trial_period | Boolean | Whether the subscription is currently in a trial period | |
user.subscriptions[].started_at | Date | Subscription start date, example: 2022-03-12T09:38:38.081Z | |
user.subscriptions[].expires_at | Date | Subscription expiration date, example: 2024-03-12T09:38:38.081Z | |
user.subscriptions[].cancelled_at | Date | Subscription cancellation date, if cancelled | |
user.subscriptions[].autorenew_enabled | Boolean | Whether auto-renew is enabled | |
user.subscriptions[].in_retry_billing | Boolean | Whether the subscription is in retry billing | |
user.subscriptions[].introductory_activated | Boolean | Whether any introductory offer, including a trial, has been used | |
user.subscriptions[].environment | String | Subscription environment | sandbox, production |
user.subscriptions[].store | String | Store where the subscription was purchased | app_store, play_store, stripe |
user.subscriptions[].kind | String | Purchase type | autorenewable, nonrenewable |
user.subscriptions[].original_transaction_id | String | App Store original_transaction_id / Google Play / Xiaomi Store purchase_token | |
user.subscriptions[].environment | String | Subscription environment | sandbox, production |
user.subscriptions[].store | String | Store where the subscription was purchased | app_store, play_store, stripe |
user.subscriptions[].kind | String | Purchase type | autorenewable, nonrenewable |
user.subscriptions[].original_transaction_id | String | App Store transaction ID / Google Play / Xiaomi Store purchase tocken | original_transaction_id / purchase_token |
user.subscriptions[].autorenew_product_id | String | Product ID that will be auto-renewed at the next billing date. Differs from product_id when the user scheduled a deferred plan change (crossgrade/downgrade) taking effect after the current period ends. From the store's pending renewal info; may be null |
user.devices[]
user.devices[]Each element of this array is a dictionary with the following structure:
| Field | Type | Description | Possible values |
|---|---|---|---|
user.devices[].id | String | Apphud device ID | |
user.devices[].device_id | String | Device identifier | |
user.devices[].device_type | String | Device model | |
user.devices[].device_family | String | Device family | |
user.devices[].platform | String | Device platform | ios, android |
user.devices[].app_version | String | Current app version on the device | |
user.devices[].sdk_version | String | Apphud SDK version used on the device | |
user.devices[].os_version | String | Operating system version | |
user.devices[].idfa | String / Null | IDFA, if available | |
user.devices[].start_app_version | String | App version installed when the user first started the app on this device | |
user.devices[].carrier | String / Null | Mobile carrier name, if available | |
user.devices[].push_token | String / Null | Push notification token, if available | |
user.devices[].idfv | String / Null | IDFV, if available | |
user.devices[].adjust_data | Object / Null | Adjust attribution data for this device, if available | |
user.devices[].appsflyer_data | Object / Null | AppsFlyer attribution data for this device, if available |
Webhooks Examples
Subscription Renewed
{
"app": {
"uid": "a1b2c3d4",
"bundle_id": "com.example.vpnapp",
"package_name": "com.example.vpnapp"
},
"event": {
"id": "11111111-2222-3333-4444-555555555555",
"created_at": "2026-03-03T16:33:05.619Z",
"properties": {
"usd_tax": 2.658333333333333,
"currency": "GBP",
"local_tax": 1.998333333333333,
"usd_price": 15.95,
"paywall_id": "pw_1234abcd",
"product_id": "com.example.vpnapp.monthly",
"local_price": 11.99,
"placement_id": "pl_5678efgh",
"usd_proceeds": 11.29791666666667,
"family_shared": false,
"local_proceeds": 8.492916666666666,
"product_bundle_id": "pb_90ab12cd",
"paywall_name": "Main Monthly Paywall",
"paywall_identifier": "paywall_main_monthly"
},
"store": "app_store",
"name": "subscription_renewed",
"receipt": {
"id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"intro_period": false,
"price_usd": 15.95,
"purchased_at": "2026-03-04T00:31:08.000Z",
"transaction_id": "500000000000001",
"original_transaction_id": "500000000000000",
"trial_period": false,
"price": 11.99,
"quantity": 1,
"apple_share": 0.15,
"proceeds_usd": 11.29791666666667,
"proceeds": 8.492916666666666,
"product_id": "com.example.vpnapp.monthly",
"currency": "GBP"
}
},
"user": {
"created_at": "2025-04-26T18:48:01.627Z",
"user_id": "USER-UUID-OBFUSCATED",
"total_spent": 176.27324951,
"payments_count": 11,
"ip": "203.0.113.42",
"uid": "user_ab12cd34",
"idfv": "IDFV-OBFUSCATED",
"country_iso_code": "GB",
"time_zone": "Europe/London",
"language": "en",
"partner_attribution": null,
"internal_attribution": {
"ad_set": "brand keywords",
"status": "Non-Organic",
"keyword": "vpn app",
"campaign": "UK_Search_Campaign",
"provider": "Apple Search Ads Integration",
"ad_network": "Apple Search Ads"
},
"variations": [],
"paying": true,
"currency_country_code": "GB",
"email": null,
"phone": null,
"name": null,
"age": null,
"gender": null,
"properties": [],
"attribution": {
"search_ads_data": {
"iad-keyword": "vpn app",
"iad-adgroup-name": "brand keywords",
"iad-campaign-name": "UK_Search_Campaign",
"iad-conversion-type": "Download",
"iad-country-or-region": "GB"
},
"search_ads_raw_data": {
"ad_id": -1,
"org_id": 1234567,
"claim_type": "Click",
"keyword_id": 1111111111,
"ad_group_id": 2222222222,
"attribution": true,
"campaign_id": 3333333333,
"conversion_type": "Download",
"country_or_region": "GB"
},
"facebook_data": null,
"tiktok_data": null,
"adjust_data": null,
"apphud_data": null
},
"subscriptions": [
{
"id": "sub_11111111-2222-3333-4444-555555555555",
"status": "regular",
"environment": "production",
"original_transaction_id": "500000000000000",
"expires_at": "2026-04-03T23:31:08.000Z",
"cancelled_at": null,
"started_at": "2025-04-26T18:48:25.000Z",
"autorenew_enabled": true,
"in_retry_billing": false,
"introductory_activated": true,
"store": "app_store",
"kind": "autorenewable",
"group": "iOS",
"product_id": "com.example.vpnapp.monthly",
"intro_period": false,
"trial_period": false
}
],
"devices": [
{
"id": "dev_11111111-2222-3333-4444-555555555555",
"device_id": "DEVICE-ID-OBFUSCATED",
"device_type": "iPhone 15 Pro",
"device_family": "iPhone",
"platform": "ios",
"app_version": "3.3.0",
"sdk_version": "3.6.2",
"os_version": "18.5",
"idfa": null,
"start_app_version": "3.0.5",
"carrier": null,
"push_token": null,
"idfv": "IDFV-OBFUSCATED",
"adjust_data": null,
"appsflyer_data": {
"af_status": "Organic",
"af_message": "organic install",
"appsflyer_id": "af_1745693274391_xxxxxx",
"is_first_launch": false
}
}
]
}
}How to Test Webhook
You can test webhooks using any service that receives HTTP events, for example, RequestBin.com.
Step 1
Open RequestBin.com and click the Create Request Bin button.
Step 2
Copy the Endpoint URL.
Step 3
Create a new Webhook in Apphud using this URL. Make sure you selected the Sandbox environment while configuring the webhook.
Step 4
Make a test purchase on your device in Sandbox environment. Read how to do this here. Whenever the webhook is triggered, you'll see a full report on RequestBin:
Updated 24 days ago
