Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 45 additions & 34 deletions docs/02_concepts/11_pay_per_event.mdx
Original file line number Diff line number Diff line change
@@ -1,84 +1,95 @@
---
id: pay-per-event
title: Pay-per-event monetization
description: Monetize your Actors using the pay-per-event pricing model
description: Monetize your Actors using the pay-per-event pricing model.
---

import ActorChargeSource from '!!raw-loader!roa-loader!./code/11_actor_charge.py';
import ConditionalActorChargeSource from '!!raw-loader!roa-loader!./code/11_conditional_actor_charge.py';
import ChargeLimitCheckSource from '!!raw-loader!roa-loader!./code/11_charge_limit_check.py';
import AdvancedChargingExample from '!!raw-loader!roa-loader!./code/11_advanced_charging.py';
import ChargePerUnitSource from '!!raw-loader!roa-loader!./code/11_charge_per_unit.py';
import ApiLink from '@theme/ApiLink';
import RunnableCodeBlock from '@site/src/components/RunnableCodeBlock';

Apify provides several [pricing models](https://docs.apify.com/platform/actors/publishing/monetize) for monetizing your Actors. The most recent and most flexible one is [pay-per-event](https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event), which lets you charge your users programmatically directly from your Actor. As the name suggests, you may charge the users each time a specific event occurs, for example a call to an external API or when you return a result.
With the [pay-per-event pricing model](https://docs.apify.com/platform/actors/publishing/monetize/pay-per-event), users pay for specific events that are programmatically triggered from your Actor's source code. Such events might include, for example, generating a single result or calling an external API.

To use the pay-per-event pricing model, first [set it up](https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event) for your Actor in the [Apify Console](https://docs.apify.com/platform/console). After that, you can start charging for events.
## Configure monetization

:::info How pay-per-event pricing works
To use the pay-per-event pricing model and define the pricing, [monetize your Actor](https://docs.apify.com/platform/actors/publishing/monetize) in Apify Console.

If you want more details about PPE pricing, please refer to our [PPE documentation](https://docs.apify.com/platform/actors/publishing/monetize/pay-per-event).
## Charge for events

:::

## Charging for events

After monetization is set in the Apify Console, you can add <ApiLink to="class/Actor#charge">`Actor.charge`</ApiLink> calls to your code and start monetizing!
To charge for events, use the <ApiLink to="class/Actor#charge">`Actor.charge`</ApiLink> method. It records that your Actor performed a billable activity, so the Apify platform charges the user's account for it.

<RunnableCodeBlock className="language-python" language="python">
{ActorChargeSource}
</RunnableCodeBlock>

Then you just push your code to Apify and that's it! The SDK will even keep track of the max total charge setting for you, so you will not provide more value than what the user chose to pay for.

If you need finer control over charging, you can call <ApiLink to="class/Actor#get_charging_manager">`Actor.get_charging_manager()`</ApiLink> to access the <ApiLink to="class/ChargingManager">`ChargingManager`</ApiLink>, which can provide more detailed information, for example how many events of each type can be charged before reaching the configured limit.
For details on how to maximize your profits, see also [Best practices](https://docs.apify.com/platform/actors/publishing/monetize/pay-per-event#best-practices).

### Handling the charge limit
### Prefer per-unit charging
Comment thread
vdusek marked this conversation as resolved.

While the SDK automatically prevents overcharging by limiting how many events are charged and how many items are pushed, **it does not stop your Actor from running**. When the charge limit is reached, <ApiLink to="class/Actor#charge">`Actor.charge`</ApiLink> and `Actor.push_data` will silently stop charging and pushing data, but your Actor will keep running — potentially doing expensive work (scraping pages, calling APIs) for no purpose. This means your Actor may never terminate on its own if you don't check the charge limit yourself.

To avoid this, you should check the `event_charge_limit_reached` field in the result returned by <ApiLink to="class/Actor#charge">`Actor.charge`</ApiLink> or `Actor.push_data` and stop your Actor when the limit is reached. You can also use the `chargeable_within_limit` field from the result to plan ahead — it tells you how many events of each type can still be charged within the remaining budget.
If you can split your work into individual units, for example scraping one page or calling one API endpoint, prefer issuing one `Actor.charge()` call per unit. Don't batch multiple events into a single call with the `count` parameter. This approach gives you better control over budget consumption:

<RunnableCodeBlock className="language-python" language="python">
{ChargeLimitCheckSource}
{ChargePerUnitSource}
</RunnableCodeBlock>

Alternatively, you can periodically check the remaining budget via <ApiLink to="class/Actor#get_charging_manager">`Actor.get_charging_manager()`</ApiLink> instead of inspecting every `ChargeResult`. This can be useful when charging happens in multiple places across your code, or when using a crawler where you don't directly control the main loop.
If you use the `count` parameter, always check the returned `charged_count`. It tells you how many events were charged, which may be less than what you requested.

:::caution
Always check the charge limit in your Actor, whether through `ChargeResult` return values or the `ChargingManager`. Without this check, your Actor will continue running and consuming platform resources after the budget is exhausted, producing no output.
:::
### Monitor charging

## Advanced charging management
For both custom and synthetic events, every `Actor.charge` call returns a <ApiLink to="class/ChargeResult">`ChargeResult`</ApiLink>. Inspect its fields to learn how much was charged.

For budget-aware crawling strategies, the <ApiLink to="class/ChargingManager">`ChargingManager`</ApiLink> (accessed via <ApiLink to="class/Actor#get_charging_manager">`Actor.get_charging_manager()`</ApiLink>) provides methods for querying the remaining budget, total charged amount, and per-event charge counts. This lets you plan work based on the remaining budget rather than discovering the limit after the fact. See the <ApiLink to="class/ChargingManager">`ChargingManager`</ApiLink> API reference for the full list of available methods.
Instead of inspecting every `ChargeResult`, you can also use the <ApiLink to="class/ChargingManager">`ChargingManager`</ApiLink>. It provides methods for querying the remaining budget, total charged amount, and per-event charge counts.

This information lets you plan work based on the remaining budget rather than discovering the limit after the fact. It's particularly useful when charging happens in multiple places across your code, or when using a crawler where you don't directly control the main loop.

To access the <ApiLink to="class/ChargingManager">`ChargingManager`</ApiLink>, use the <ApiLink to="class/Actor#get_charging_manager">`Actor.get_charging_manager()`</ApiLink> method:

<RunnableCodeBlock className="language-python" language="python">
{AdvancedChargingExample}
</RunnableCodeBlock>

## Transitioning from a different pricing model
### Handle the charge limit

When you plan to start using the pay-per-event pricing model for an Actor that is already monetized with a different pricing model, your source code must support both pricing models during the transition period enforced by the Apify platform. The most frequent case is the transition from the pay-per-result model which utilizes the `ACTOR_MAX_PAID_DATASET_ITEMS` environment variable to prevent returning unpaid dataset items. The following is an example how to handle such scenarios. The key part is the <ApiLink to="class/ChargingManager#get_pricing_info">`ChargingManager.get_pricing_info()`</ApiLink> method which returns information about the current pricing model.
Your Actor users can set the maximum cost for the run. It helps users control their spending, as they won't be billed beyond the limit.

The user spending limit for the run is available in your Actor code as the `ACTOR_MAX_TOTAL_CHARGE_USD` environment variable, and <ApiLink to="class/ChargeResult">`ChargeResult`</ApiLink> already accounts for it. To control the limit, inspect the fields of `ChargeResult`:

- `event_charge_limit_reached`: Checks if the user's limit allows for another charge of the event.
- `chargeable_within_limit`: Indicates how many events of each type can still be charged within the remaining budget.
- `charged_count`: Indicates how many events were billed by the call.

<RunnableCodeBlock className="language-python" language="python">
{ConditionalActorChargeSource}
{ChargeLimitCheckSource}
</RunnableCodeBlock>

## Local development
When the charge limit is reached, <ApiLink to="class/Actor#charge">`Actor.charge`</ApiLink> stops charging and <ApiLink to="class/Actor#push_data">`Actor.push_data`</ApiLink> stops pushing data. 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](https://docs.apify.com/platform/actors/publishing/monetize/pay-per-event#handle-graceful-shutdown).

## Test monetization locally

It is encouraged to test your monetization code on your machine before releasing it to the public. To tell your Actor that it should work in pay-per-event mode, pass it the `ACTOR_TEST_PAY_PER_EVENT` environment variable:
Before releasing your monetization code to the public, test it locally. To make your Actor work in pay-per-event mode, pass it the `ACTOR_TEST_PAY_PER_EVENT` environment variable:

```sh
ACTOR_TEST_PAY_PER_EVENT=true python -m youractor
```

If you also wish to see a log of all the events charged throughout the run, the Apify SDK keeps a log of charged events in a so called charging dataset. Your charging dataset can be found under the `charging-log` name (unless you change your storage settings, this dataset is stored in `storage/datasets/charging-log/`). Please note that this log is not available when running the Actor in production on the Apify platform.
In this mode, nothing is billed, but every charge call is logged into a local `charging-log` dataset.

### View the log

To inspect the results of your tests, open the `charging-log` dataset. By default, it's stored in the `storage/datasets/charging-log/` directory.

Because pricing configuration is stored by the Apify platform, all events will have a default price of $1.
The log contains all the events charged throughout the run. Because pricing configuration is stored by the Apify platform, all events have a default price of $1.

## Conclusion
Note that this log isn't available when running the Actor in production on the Apify platform.

This page has covered the pay-per-event pricing model: charging for events with <ApiLink to="class/Actor#charge">`Actor.charge`</ApiLink>, respecting the charge limit so your Actor stops once the budget is exhausted, querying the <ApiLink to="class/ChargingManager">`ChargingManager`</ApiLink>, handling the transition from another pricing model, and testing charging locally.
## Transition from a different pricing model

For comprehensive details on pay-per-event pricing and Actor monetization, see the [pay-per-event](https://docs.apify.com/platform/actors/publishing/monetize/pay-per-event) and [monetization](https://docs.apify.com/platform/actors/publishing/monetize) documentation on the Apify platform.
When you plan to start using the pay-per-event pricing model for an Actor that is already monetized with a different pricing model, your source code must support both pricing models during the transition period enforced by the Apify platform. The most frequent case is the transition from the pay-per-result model which utilizes the `ACTOR_MAX_PAID_DATASET_ITEMS` environment variable to prevent returning unpaid dataset items. The following is an example how to handle such scenarios. The key part is the <ApiLink to="class/ChargingManager#get_pricing_info">`ChargingManager.get_pricing_info()`</ApiLink> method which returns information about the current pricing model.

<RunnableCodeBlock className="language-python" language="python">
{ConditionalActorChargeSource}
</RunnableCodeBlock>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't there be some conclusion?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About that conclusion, where does it come from? We don't use it anywhere in the docs except for here. Frankly, it's not really useful. Is it some legacy?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, it was already part of the existing content when I joined, so I just followed the established structure.

Removing it from a single page and leaving it on the others is inconsistent. If we decide that these sections are unnecessary, we should apply that change across the whole documentation.

As for whether they should be there at all, I'm in favor of keeping them. IMO learning-oriented content benefits from an ending that summarizes the key takeaways or points readers toward what to explore next.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I brought it up at the docs meeting, we discussed it, and decided that we should get rid of the Conclusion section everywhere. The main reasons are consistency with the rest of the docs plus these sections don't add much value in the current form. These docs aren't a course to follow, they aren't read like a book.

I'll review all these sections and move some of the content to the introductions, where it makes sense.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, please leave it in this PR for now and open a follow-up PR to remove them, since it's unrelated to these changes.

2 changes: 1 addition & 1 deletion docs/02_concepts/code/11_charge_limit_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async def main() -> None:
result = {'url': url, 'data': f'Scraped data from {url}'}

# highlight-start
# push_data returns a ChargeResult - check it to know if the budget ran out
# push_data returns a ChargeResult, check it to know if the budget ran out
charge_result = await Actor.push_data(
result, charged_event_name='result-item'
)
Expand Down
30 changes: 30 additions & 0 deletions docs/02_concepts/code/11_charge_per_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import asyncio

from apify import Actor


async def main() -> None:
async with Actor:
urls = [
'https://example.com/1',
'https://example.com/2',
'https://example.com/3',
]

for url in urls:
# Charge for a single event
charge_result = await Actor.charge(
event_name='page-scraped',
)

if charge_result.event_charge_limit_reached:
break

result = {'url': url, 'data': f'Scraped data from {url}'}

# Push the result to the dataset
await Actor.push_data(result)


if __name__ == '__main__':
asyncio.run(main())
Loading
Loading