Set transaction with server-side API
Creates a new transaction for an end user of your app in Adapty and provides access level. The transaction created by this method will appear in your analytics and Event Feed and well as will be sent to all integrations.
This method is recommended over the Grant access level one.
If the product is not created in Adapty, the transaction will still be recorded in the Adapty database, meaning it will appear in analytics and be included in integration events.
However, the user will get only the default premium
access level. So, if you want to control access more flexibly and assign different access levels, you must create products in Adapty.
Method and endpoint
POST https://api.adapty.io/api/v2/server-side-api/purchase/set/transaction/
Varies based on whether the purchase is a subscription or a one-time purchase.
Example request
- cURL
- Python
- JavaScript
curl --location 'https://api.adapty.io/api/v2/server-side-api/purchase/set/transaction/' \
--header 'Authorization: Api-Key <YOUR_SECRET_API_KEY>' \
--header 'adapty-customer-user-id: <YOUR_CUSTOMER_USER_ID>' \
--header 'Content-Type: application/json' \
--data '{
"purchase_type": "subscription",
"store": "app_store",
"environment": "Production",
"store_product_id": "1year.premium",
"store_transaction_id": "30002109551456",
"store_original_transaction_id": "30002109551456",
"is_family_shared": false,
"price": {
"country": "US",
"currency": "USD",
"value": 9.99
},
"purchased_at": "2022-10-12T09:42:50.000000+0000",
"variation_id": "81109d24-ea95-4806-9ec7-b482bbd1a33d",
"originally_purchased_at": "2022-10-12T09:42:50.000000+0000",
"renew_status": true,
"expires_at": "2026-10-12T09:42:50.000000+0000"
}'
import requests
url = "https://api.adapty.io/api/v2/server-side-api/purchase/set/transaction/"
payload = {
"purchase_type": "subscription",
"store": "app_store",
"environment": "Production",
"store_product_id": "1year.premium",
"store_transaction_id": "30002109551456",
"store_original_transaction_id": "30002109551456",
"is_family_shared": False,
"price": {
"country": "US",
"currency": "USD",
"value": 9.99
},
"purchased_at": "2022-10-12T09:42:50.000000+0000",
"variation_id": "81109d24-ea95-4806-9ec7-b482bbd1a33d",
"originally_purchased_at": "2022-10-12T09:42:50.000000+0000",
"renew_status": True,
"expires_at": "2026-10-12T09:42:50.000000+0000"
}
headers = {
"Authorization": "Api-Key <YOUR_SECRET_API_KEY>",
"adapty-customer-user-id": "<YOUR_CUSTOMER_USER_ID>",
"Content-Type": "application/json"
}
response = requests.post(url, headers=headers, json=payload)
print(response.text)
const myHeaders = new Headers();
myHeaders.append("adapty-customer-user-id", "<YOUR_CUSTOMER_USER_ID>");
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Authorization", "Api-Key <YOUR_SECRET_API_KEY>");
const raw = JSON.stringify({
"purchase_type": "subscription",
"store": "app_store",
"environment": "Production",
"store_product_id": "1year.premium",
"store_transaction_id": "30002109551456",
"store_original_transaction_id": "30002109551456",
"is_family_shared": false,
"price": {
"country": "US",
"currency": "USD",
"value": 9.99
},
"purchased_at": "2022-10-12T09:42:50.000000+0000",
"variation_id": "81109d24-ea95-4806-9ec7-b482bbd1a33d",
"originally_purchased_at": "2022-10-12T09:42:50.000000+0000",
"renew_status": true,
"expires_at": "2026-10-12T09:42:50.000000+0000"
});
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
fetch("https://api.adapty.io/api/v2/server-side-api/purchase/set/transaction/", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result))
.catch((error) => console.error(error));
Placeholders:
<YOUR_CUSTOMER_USER_ID>
: The unique ID of the customer in your system.<YOUR_SECRET_API_KEY>
: Your secret API key for authorization.
For subscription
Parameter | Type | Required | Nullable | Description |
---|---|---|---|---|
purchase_type | String | Yes | No | The type of product purchased. Possible value: subscription . |
store | String | Yes | No | Store where the product was bought. Options include app_store , play_store , stripe , or the Store ID of your custom store. |
environment | String | No | No | Environment where the transaction took place. Options are Sandbox or Production . Production is used by default. |
store_product_id | String | Yes | No | ID of the product in the app store (App Store, Google Play, Stripe, etc.) that unlocked this access level. |
store_transaction_id | String | Yes | No | Transaction ID in the app store (App Store, Google Play, Stripe, etc.). |
store_original_transaction_id | String | Yes | No | For subscriptions, this ID links to the first transaction in a renewal chain. Each renewal is connected to this original transaction. If there's no renewal, |
offer | Object | No | Yes | The offer used in the purchase, provided as an Offer object. |
is_family_shared | Boolean | No | No | A Boolean value indicating whether the product supports family sharing in App Store Connect. iOS only. Always false for iOS below 14.0 and macOS below 11.0. false is used by default. |
price | Object | Yes | No | Price of the subscription or purchase as a Price object. An initial subscription purchase with zero cost is a free trial; a renewal with zero cost is a free renewal. |
purchased_at | ISO 8601 date | Yes | No | The datetime of the most recent access level purchase. |
refunded_at | ISO 8601 date | No | No | The datetime when the subscription was refunded, if applicable. |
cancellation_reason | String | No | No | Possible reasons for cancellation include: voluntarily_cancelled , billing_error , price_increase , product_was_not_available , refund , upgraded , or unknown . |
variation_id | String | No | No | The variation ID used to trace purchases to the specific paywall they were made from. |
originally_purchased_at | ISO 8601 date | Yes | No | For subscription chains, this is the purchase date of the original transaction, linked by store_original_transaction_id . |
expires_at | ISO 8601 date | Yes | No | The datetime when the access level expires. It may be in the past and null for lifetime access. |
renew_status | Boolean | Yes | No | Indicates if auto-renewal is enabled for the subscription. |
renew_status_changed_at | ISO 8601 date | No | No | The datetime when auto-renewal was either enabled or disabled. |
billing_issue_detected_at | ISO 8601 date | No | No | The datetime when a billing issue was detected (e.g., a failed card charge). The subscription might still be active. This is cleared if the payment goes through. |
grace_period_expires_at | ISO 8601 date | No | No | The datetime when the grace period will end if the subscription is currently in one. |
For one-time purchase
Parameter | Type | Required | Nullable | Description |
---|---|---|---|---|
purchase_type | String | Yes | No | The type of product purchased. Possible value: one_time_purchase . |
store | String | Yes | No | Store where the product was bought. Possible values: app_store , play_store , stripe , or the Store ID of your custom store. |
environment | String | No | No | Transaction environment that provided the access level. Options: Sandbox , Production . Production is used by default. |
store_product_id | String | Yes | No | The product ID in the app store (App Store, Google Play, Stripe, etc.) that unlocked this access level. |
store_transaction_id | String | Yes | No | Transaction ID in the app store (App Store, Google Play, Stripe, etc.). |
store_original_transaction_id | String | Yes | No | For recurring subscriptions, this is the original transaction ID that links the chain of renewals. The original transaction is the first in the chain; later transactions are renewals. If there's no renewal, |
offer | Object | No | Yes | The offer used for the purchase as an Offer object. |
is_family_shared | Boolean | No | No | A Boolean value indicating whether the product supports family sharing in App Store Connect. iOS only. Always false for iOS below 14.0 and macOS below 11.0. false is used by default. |
price | Object | Yes | No | Price of the one-time purchase as a Price object. An initial subscription purchase with zero cost is a free trial; a renewal with zero cost is a free renewal. |
purchased_at | ISO 8601 date | Yes | No | The datetime when the access level was last purchased. |
refunded_at | ISO 8601 date | No | No | If refunded, shows the datetime of the refund. |
cancellation_reason | String | No | No | Possible reasons for cancellation: voluntarily_cancelled , billing_error , price_increase , product_was_not_available , refund , cancelled_by_developer , new_subscription , unknown . |
variation_id | String | No | No | The variation ID used to trace purchases to the specific paywall they were made from. |
Successful response: 200: OK
The request is successful. The response body contains the data
field, which encapsulates the user's profile and associated information.
Parameter | Type | Nullable | Description |
---|---|---|---|
data | Object | ➖ | Contains the Profile object with user details and metadata. |
data object structure
The data
field is the primary container for the user profile. It includes several fields:
Parameter | Type | Nullable | Description |
---|---|---|---|
app_id | String | ➖ | The internal ID of your app. You can see in the the Adapty Dashboard: App Settings -> General tab. |
profile_id | UUID | ➖ | Adapty profile ID. You can see it in the Adapty ID field on the Adapty Dashboard -> Profiles -> specific profile page. |
customer_user_id | String | ➕ | The ID of your user in your system. You can see it in the Customer user ID field on the Adapty Dashboard -> Profiles -> specific profile page. It will work only if you identify the users in your mobile app code via Adapty SDK. |
total_revenue_usd | Float | ➖ | A float value representing the total revenue in USD earned in the profile. |
segment_hash | String | ➖ | Internal parameter. |
timestamp | Integer | ➖ | Response time in milliseconds, needs for resolve a race condition. |
custom_attributes | Array | ➖ | A maximum of 30 custom attributes to the profile are allowed to be set. If you provide the Key: The key must be a string with no more than 30 characters. Only letters, numbers, dashes, points, and underscores allowed Value: The attribute value must be no more than 30 characters. Only strings and floats are allowed as values, booleans will be converted to floats. Send an empty value or null to delete the attribute. |
access_levels | Array | ➕ | Array of Access level objects. Can be null if the customer has no access levels. |
subscriptions | Array | ➕ | Array of Subscription objects. Can be null if the customer has no subscriptions. |
non_subscriptions | Array | ➕ | Array of Non-Subscription objects. Can be null if the customer has no purchases. |
Successful response example
{
"data": {
"app_id": "14c3d623-2f3a-455a-aa86-ef83dff6913b",
"profile_id": "3286abd3-48b0-4e9c-a5f6-ac0a006804a6",
"customer_user_id": "[email protected]",
"total_revenue_usd": 0.0,
"segment_hash": "8f45947bad31ab0c",
"timestamp": 1736436751469,
"custom_attributes": [
{
"key": "favourite_sport",
"value": "yoga"
}
],
"access_levels": [],
"subscriptions": [
{
"purchase_id": "5a7ab471-2299-45f7-ad69-1d395c1256e3",
"store": "app_store",
"store_product_id": "1year.premium",
"store_base_plan_id": null,
"store_transaction_id": "30002109551456",
"store_original_transaction_id": "30002109551456",
"purchased_at": "2022-10-12T09:42:50+00:00",
"environment": "Production",
"is_refund": false,
"is_consumable": false
}
],
"non_subscriptions": []
}
}
Errors
400: Bad request
billing_issue_detected_at_date_comparison_error
A billing issue happens when there’s a problem during a subscription renewal attempt, so it always occurs after the transaction date (purchased_at
).
To resolve this, make sure the billing issue date (billing_issue_detected_at
) is later than the transaction date (purchased_at
).
Body
Parameter | Type | Description |
---|---|---|
errors | Object |
|
error_code | String | Short error name. Always billing_issue_detected_at_date_comparison_error . |
status_code | Integer | HTTP status. Always 400 . |
Response example
{
"errors": [
{
"source": "billing_issue_detected_at",
"errors": [
"billing_issue_detected_at must be later than purchased_at."
]
}
],
"error_code": "billing_issue_detected_at_date_comparison_error",
"status_code": 400
}
expires_date_error
A user can’t buy a subscription that has already expired. So, the expires_at
date (when the subscription expires) should always be later than the purchased_at
date (when the transaction occurred).
To fix this, check these dates and ensure that expires_at
is after purchased_at
.
Body
Parameter | Type | Description |
---|---|---|
errors | Object |
|
error_code | String | Short error name. Always expires_date_error . |
status_code | Integer | HTTP status. Always 400 . |
Response example
{
"errors": [
{
"source": "expires_at",
"errors": [
"expires_at must be later than purchased_at."
]
}
],
"error_code": "expires_date_error",
"status_code": 400
}
family_share_price_error
The request failed because the is_family_shared
parameter is set to true
, meaning the access level is shared with a family member for free. However, the value
parameter of the Price object isn’t set to zero.
If is_family_shared
should be true
, make sure to set the value
parameter of the Price object to 0
.
Body
Parameter | Type | Description |
---|---|---|
errors | Object |
|
error_code | String | Short error name. Always: family_share_price_error . |
status_code | Integer | HTTP status. Always 400 . |
Response example
The profile is not found
{
"errors": [
{
"source": "is_family_shared",
"errors": [
"If is_family_shared is true, price.value must be 0."
]
}
],
"error_code": "family_share_price_error",
"status_code": 400
}
free_trial_price_error
The request failed because the offer_type
parameter is set to free_trial
, but the value
parameter of the Price object isn’t set to zero.
Another possible reason is that the offer_id
parameter was included but left null
, even though it can’t be null. In this case, either provide a value for offer_id
or remove the parameter entirely.
Body
Parameter | Type | Description |
---|---|---|
errors | Object |
|
error_code | String | Short error name. Always: free_trial_price_error . |
status_code | Integer | HTTP status. Always 400 . |