Pay per event pricing model
The pay per event (PPE) pricing model offers a flexible monetization option for Actors on Apify Store. The key benefits are:
- Unlimited revenue scalability
- AI/MCP compatibility
- Predictable user costs
- Store discounts available
- Custom event billing
- Two automatically charged events available
About events
An event is a specific, measurable action that your Actor triggers programmatically and charges the user for. The most common events include:
- Creating a result.
- Processing an item.
- Calling an external API.
You define the events and can set different pricing for each event type and discount tier.
Synthetic events
Synthetic events are predefined by Apify and charged automatically:
apify-actor-startreduces the costs of starting the Actor.apify-default-dataset-itemcharges for each item written to the default dataset of the run.
You can remove synthetic events or change their price in Apify Console.
Custom events
To charge users for the unique value that your Actor delivers, define custom events. You name each event, describe it, set its price, and charge it from your Actor's code.
Get event names
To retrieve the list of all chargeable event names for your Actor, use the Get Actor API endpoint.
Calculate your profit
Your profit is calculated from the following formula:
profit = (0.8 * revenue) - platform costs
where:
revenueis the amount charged for events through the PPE charging API or JS/Python SDK. You receive 80% of this revenue.platform costsare the underlying platform usage costs for running the Actor. For details, see Compute costs for PPE Actors.
Only revenue and cost for Apify customers on paid plans are taken into consideration when computing your profit. Users on free plans aren't reflected there.
Avoid negative profit
To make your Actor profitable, price your events so that the revenue covers the platform costs. For details, see Compute costs for PPE Actors.
If your Actor produces negative profit, make sure to:
When testing your Actor pricing, you can also temporarily pass platform usage costs on to users.
An Actor's negative net profit doesn't affect the positive profit of another Actor. For aggregation purposes, an Actor with a negative net profit is considered to have a profit of $0.
- Without isolation:
Total profit = (-$90) + $100 = $10 - With isolation:
Total profit = $0 + $100 = $100
Set pricing for PPE Actors
- Understand your costs: Analyze resource usage (e.g CPU, memory, proxies, external APIs) and identify cost drivers.
- Define clear events: break your Actor's functionality into measurable, chargeable events.
- Choose a primary event: Pick the event that best represents the main value of your Actor. This primary event is highlighted on the Actor detail page, so users quickly understand what they are mainly paying for.
- Common use cases:
- For scraping: combine Actor start and dataset items pricing to reflect setup and per-result cost.
- Beyond scraping: Account for integrations with external systems or external API calls.
- External API costs: Account for additional processing costs.
- Test your pricing: Run your Actor and analyze cost-effectiveness using a special dataset.
- Communicate value: Ensure pricing reflects the value provided and is competitive.
Pass platform usage costs on to users
To avoid negative profit margins during initial development or when testing pricing strategies, you can pass the platform usage costs on to users. As a result, users pay for both the defined events and the underlying platform usage.
Charging platform usage separately is less transparent for users and may discourage them from using your Actor. It also negatively impacts your Actor quality score. Use this approach only until you determine accurate event prices.
To pass platform usage costs on to users:
- Go to the Actor pricing step of the monetization setup.
- In Additional options, switch on the Pay per event + usage toggle.
Turning this option on takes 14 days to take effect. However, you can turn it off immediately, as it is a positive change for users.
Best practices
To simplify PPE implementation into you Actor, use the Apify SDKs. They help you handle pricing, usage tracking, idempotency keys, API errors, and event charging through an API. Without the SDKs, you must handle API integration and possible edge cases manually.
To implement event charging with the Apify CLI, use the apify actor charge command.
You can also call the Charge events in run API endpoint directly.
Respect user spending limits
Your Actor users can set the maximum cost for the run in Apify Console or with the API. It helps users control their spending, as they won't be billed beyond the limit.
The Apify platform enforces this limit. Once it's reached, Actor.charge() stop charging and Actor.pushData() stops pushing data over the limit. The platform then aborts the run automatically. However, the run keeps consuming platform resources for a short time before it stops. For details, see Handle graceful shutdown.
To protect your profits, finish the Actor run once charges reach the user's limit.
To prevent users from setting the maximum cost for the run too low, set the minimum value during the monetization setup. For example, you can use it to cover the price of starting the Actor and producing one result.
The minimum value is stored by the minimalMaxTotalChargeUsd property.
The user spending limit is available in your Actor code as the ACTOR_MAX_TOTAL_CHARGE_USD environment variable, and ChargeResult already accounts for it. Returning a ChargeResult depends on the event type:
- For custom events,
Actor.charge()andActor.pushData()called with an event name always return aChargeResult. - For synthetic events, pushing datasets items is charged automatically. In the Python SDK,
Actor.pushData()returns aChargeResult. In the JavaScript SDK,Actor.pushData()called without an event name returnsvoid.
Inspect the ChargeResult properties to decide when to stop the run:
| Property | Description |
|---|---|
eventChargeLimitReached | Checks if the user's limit allows for another charge of the event. |
chargeableWithinLimit | Indicates how many more of each event you can still charge within the remaining budget. Use it if you have multiple events. |
chargedCount | Indicates how many events were billed by the call. For batch charges (count), it can be lower than you requested. Charges above chargedCount are not billed. |
ACTOR_MAX_TOTAL_CHARGE_USD and the ChargeResult limit apply to a single run, not to a user's total spending. If a user starts many runs at once, your combined platform costs across all runs can grow beyond what a single run's limit suggests. To protect your profit, set memory limits and keep per-event platform costs low.
Make sure to:
- Charge for work as soon as it's done, not in a large batch at the end. If the run stops early, produced results don't get charged.
- Charge for results after they're saved and available for users. Don't bill users for what they can't access, for example if a run gets aborted.
- Finish the run once
eventChargeLimitReachedistrue.
ChargingManagerIf charging happens in multiple places across your code, or when you use an Actor where you don't directly control the main loop, you can use the ChargingManager to check the remaining budget periodically instead of inspecting every ChargeResult.
- JavaScript
- Python
import { Actor } from 'apify';
const chargeForApiProductDetail = async () => {
const chargeResult = await Actor.charge({ eventName: "product-detail" });
return chargeResult;
};
await Actor.init();
// API call, or any other logic that you want to charge for
const chargeResult = await chargeForApiProductDetail();
if (chargeResult.eventChargeLimitReached) {
await Actor.exit();
}
// Rest of the Actor logic
await Actor.exit();
from apify import Actor
async def charge_for_api_product_detail():
charge_result = await Actor.charge(event_name='product-detail')
return charge_result
async def main():
await Actor.init()
# API call, or any other logic that you want to charge for
charge_result = await charge_for_api_product_detail()
if charge_result.event_charge_limit_reached:
await Actor.exit()
# Rest of the Actor logic
await Actor.exit()
When using Crawlee, use crawler.autoscaledPool.abort() instead of Actor.exit() to gracefully finish the crawler and let the rest of your code run normally.
Handle graceful shutdown
When a platform or user gracefully aborts a run, the platform sends the aborting event and force-stops the run 30 seconds later. Use this window to bill for work already done, so an abort doesn't cost you the revenue.
Apify SDKs already handle the graceful shutdown for you. They listen for aborting and call Actor.exit() automatically. Register your own aborting handler to:
- Charge for completed but unbilled work with
Actor.charge(). - Push buffered results with
Actor.pushData(). - Persist state so a resurrected run can resume instead of redoing paid work.
- Close open resources, such as browsers, database connections, or files.
Your handler runs alongside the SDK's, and Actor.exit() waits for all event listeners to finish before terminating, so your cleanup completes within the 30-second window.
- JavaScript
- Python
import { Actor } from 'apify';
await Actor.init();
Actor.on('aborting', async () => {
// Charge for completed but unbilled work
});
// Rest of the Actor logic
await Actor.exit();
from apify import Actor, Event
async def main():
await Actor.init()
async def on_aborting(_event_data=None):
# Charge for completed but unbilled work
...
Actor.on(Event.ABORTING, on_aborting)
# Rest of the Actor logic
await Actor.exit()
A non-graceful abort stops the run immediately, without emitting aborting, so your handler never runs. Don't rely on it and charge for work as soon as it's done. For details, see Respect user spending limits.
Use apify-actor-start
The apify-actor-start synthetic event reduces your startup costs and makes your Actor pricing more competitive. When enabled, Apify covers the compute cost of the first 5 seconds of every run.
Apify recommends using the synthetic apify-actor-start event in all Actors. It benefits both you and your users.
The apify-actor-start synthetic event works as follows:
- It's available by default for all new PPE Actors. For existing Actors, you can enable it in Apify Console.
- Apify automatically charges the event. Don't manually charge the event in your Actor code, as it will fail.
- The default price of the event is $0.00005, which equals to $0.05 per 1,000 starts. To keep your Actor competitive, Apify recommends keeping the default price.
- The number of events charged depends on the amount of memory allocated to the run. Up to and including 1 GB of RAM, the event is charged once. Then, it's charged once for each extra GB of memory. For example:
- 128 MB RAM: 1 event, $0.00005
- 1 GB RAM: 1 event, $0.00005
- 4 GB RAM: 4 events, $0.0002
- You can increase the price of the event, but you won't get more free compute.
- You can delete the event, but then you will lose the free 5 seconds of compute.
Synthetic start event for new and existing Actors
For new Actors, the apify-actor-start event is added automatically.
For existing Actors, you can add this event manually:
- Access your Actor's monetization setup.
- In the Actor pricing step, under Event name, add
apify-actor-start. - Complete the remaining event details: title, description, and price.
Actors with a custom start event
Don't use both the synthetic and custom start events.
To use the synthetic event, remove the existing custom event from your Actor in Apify Console.
Use apify-default-dataset-item
The apify-default-dataset-item synthetic event lets you align PPE pricing with per-result use cases:
- It automatically charges users for each item that your Actor writes to the default dataset. For example, when using
Actor.pushData(). - It applies only to the default dataset of the run.
- It's enabled by default, you don't need to add extra charging code to your Actor.
To stop charging for default dataset items automatically, remove this event from your Actor in Apify Console.
Set memory limits
Memory allocation is the user's choice. By setting an upper limit, you prevent users from selecting more memory than your Actor needs and avoid higher platform costs.
To set memory limits, use the minMemoryMbytes and maxMemoryMbytes properties in your actor.json file:
{
"actorSpecification": 1,
"name": "name-of-my-actor",
"version": "0.0",
"minMemoryMbytes": 512,
"maxMemoryMbytes": 1024,
}
When using browser automation tools like Puppeteer or Playwright for web scraping, increase the memory limits to accommodate the browser's memory usage.
Charge for invalid input
Charge for inputs that seem valid but lead to errors, such as URLs that return a 404. Even though such action doesn't produce usable data, you still spend platform resources opening the page to discover the problem.
Note that charging for invalid input might not cover the entire cost of platform resources spent on handling errors. Test your Actor with invalid input and adjust the validation logic, if necessary.
Instead of failing the entire run because of the bad input, return error items with clear error codes and messages. The run continues for the valid inputs and gives users a transparent record of what they were charged for.
The following example shows how to charge for invalid inputs using Actor.pushData() when a dataset item is created and the scraped-result event is charged:
- JavaScript
- Python
import { Actor } from 'apify';
const processUrl = async (url) => {
const response = await fetch(url);
if (response.status === 404) {
// Charge for the work done and return error item in one call
await Actor.pushData({
url: url,
error: "404",
errorMessage: "Page not found"
}, 'scraped-result');
return;
}
// Rest of the process_url function
};
await Actor.init();
const input = await Actor.getInput();
const { urls } = input;
for (const url of urls) {
await processUrl(url);
}
// Rest of the Actor logic
await Actor.exit();
from apify import Actor
import requests
async def process_url(url):
response = requests.get(url)
if response.status_code == 404:
# Charge for the work done and return error item in one call
await Actor.push_data({
'url': url,
'error': '404',
'errorMessage': 'Page not found'
}, 'scraped-result')
return
# Rest of the process_url function
async def main():
await Actor.init()
input_data = await Actor.get_input()
urls = input_data.get('urls', [])
for url in urls:
await process_url(url)
# Rest of the Actor logic
await Actor.exit()
Keep pricing simple
Limit the number of events. Fewer events make it easier for users to understand your pricing and predict the costs.
Avoid cases where producing a single result triggers too many events. It makes your Actor more expensive.
Make events produce visible results
Don't charge users for actions they don't see. For Actors that produce data, events should map to concrete results in the user's dataset or storage.
You can still implement events that don't produce tangible results, such as running AI workflows or processing external API calls. In such cases, inform users about actions performed by your Actor by displaying a status message or pushing a record to the dataset. It helps users understand what actions they were charged for.
The following examples show clear mapping of events to results:
postevent: Each charge adds one social media post to the dataset.profileevent: Each charge adds one user profile to the dataset.processed-imageevent: Each charge adds one processed image to the dataset.ai-analysisevent: Each charge processes one document through an AI workflow.
Use idempotency keys
Apify SDKs handle idempotency for you.
If you're not using the Apify SDKs, implement idempotency to prevent charging for the same event multiple times.
Example of a PPE pricing
You create a social media monitoring Actor with the following pricing:
post: $0.002 per post - count every social media post you extract.profile: $0.005 per profile - count every user profile you extract.sentiment-analysis: $0.01 per post - count every post analyzed for sentiment, engagement metrics, and content classification using external LLM APIs.
You have two main strategies for charging AI-related operations:
- Fixed event pricing (like
sentiment-analysisabove): Charge a fixed amount per operation, regardless of actual LLM costs - Usage-based pricing: Use events like
llm-tokenthat charge based on actual LLM usage costs
Fixed pricing is simpler for users to predict, while usage-based pricing more accurately reflects your actual costs.
Pricing breakdown by user
| User | Plan | Events | Charges | Total | Platform cost |
|---|---|---|---|---|---|
| 1 | Paid plan | 5,000 × post1,000 × sentiment-analysis | 5,000 × $0.002 1,000 × $0.01 | $20 | $2.50 |
| 2 | Paid plan | 3,000 × post500 × sentiment-analysis | 3,000 × $0.002 500 × $0.01 | $11 | $1.50 |
| 3 | Free plan | 1,000 × post100 × sentiment-analysis | 1,000 × $0.002 100 × $0.01 | $3 | $0.40 |
Your profit and costs are computed only from the first two users since they are on Apify paid plans.
The platform usage costs are just examples, but you can see the actual costs in the Computing your costs for PPE Actors section.
Revenue breakdown
- Revenue (paid users only): $20 + $11 = $31
- Platform cost (paid users only): $2.50 + $1.50 = $4
- Profit: 0.8 × $31 − $4 = $20.80
Next steps
- Check out the Pricing and costs section to learn how to compute your costs.