# Abuse Procedure Source: https://bunny.net/docs/account/abuse-procedure How bunny.net handles abuse reports for content hosted or delivered through our platform. bunny.net is a platform service provider acting as a passive distributor of third-party information. We are not a publisher and do not moderate content except as set out in our published procedures (updated from time to time). bunny.net takes no responsibility to our Customers or users for data and content uploaded by Customers or third parties, and is not liable for that content. We take abuse reports seriously while giving customers a fair opportunity to review and address complaints. This procedure outlines how we handle general abuse reports, including how we verify complaints and how customers can respond. > [!NOTE] > > We operate on a good-faith, voluntary basis, aiming to meet industry best practice while taking into account relevant national and international regulatory guidance and applicable legislation. We have no obligation to respond to allegations of abuse or to take action, and we are not liable for our acts or omissions other than as set out in our [Terms of Service](https://bunny.net/tos). We reserve the right to amend or withdraw this procedure at any time. ## What is abuse? We consider the following to be abuse, or complaints of alleged abuse: * Misinformation (sometimes known as "fake news") * Violation of our Terms of Service (such as publishing illegal content, or harmful activity such as hacking, phishing, or malware) * Violent threats and harassment * Child Sexual Abuse Material (CSAM) Misinformation may be described as verifiably false or misleading information created, presented, and disseminated for economic gain or to intentionally deceive the public, and which may cause public harm. We do **not** consider the following to be abuse: * Disagreements over political or news articles * Attempts to silence commentary or alternative points of view * Defamatory, aggressive, or insulting content * Reviews of service providers or products ## Our process Reports can be submitted via our [abuse form](https://bunny.net/abuse/). ### Receiving and reviewing the report When we receive an abuse report related to content hosted or delivered through our platform, our team reviews it to determine: * Whether the allegation is valid and specific (vague or generic complaints may require further clarification) * Whether the content violates applicable laws or our Acceptable Use Policy We do not take immediate action unless the content is clearly illegal (such as phishing, malware, or CSAM). For all other cases, we first notify the customer and allow them to review the allegations. ### Notifying the customer Once we verify the report, we notify the customer responsible for the content by email, and the report is visible in the customer dashboard. The notice includes: * A summary of the allegation, including the details provided by the reporter * A request to review the report and take action if necessary * A response timeline, typically 48 hours for all abuse reports except those related to misinformation Until the response period has passed, no action is taken on the content. We do not, as a matter of routine, provide the customer with personal data relating to the complainant. ### Customer response options * **Address the report:** Modify or remove the content, or provide additional information to clarify the situation. * **Dispute the report:** Explain why the content should not be removed, and share any supporting documentation (such as licenses or legal permissions). We review the response and, if necessary, work with the customer to ensure compliance with applicable regulations or our Acceptable Use Policy. ### No immediate suspension or blocking Unlike DMCA or CSAM reports, we do not suspend Pull Zones or remove content for general abuse allegations. Our goal is to work with the customer to verify and resolve the issue. However, if a customer fails to respond in time, or the content clearly violates our policies, we may take further action. ### Repeat offenses If a customer repeatedly receives abuse reports and does not take appropriate action, we may require faster response times for future reports, restrict access to certain services or features, or suspend or terminate the account. We always attempt to work with the customer first before escalating. ## Frequently asked questions **Will my Pull Zone or content be suspended for fake news or misinformation?** No. We do not suspend Pull Zones or remove content solely based on abuse reports alleging fake news or misinformation. **Will my Pull Zone or content be suspended immediately?** No. We do not suspend Pull Zones or remove content immediately for general abuse reports. We give customers an opportunity to review and address the complaint first. **What happens if I don't respond to an abuse report?** If no response is received within the given timeframe, we may take further action, including potential suspension of the specific URL. This depends on the type of abuse report. **Can I dispute an abuse report?** Yes. If you believe a report is incorrect or invalid, submit a response explaining why, along with supporting evidence. bunny.net will review the response and decide, at its own discretion and with no liability, whether to uphold the allegation or reinstate the content in whole or in part. **What types of abuse reports require immediate action?** Reports related to illegal activity (such as CSAM, phishing, or malware) may require immediate content blocking and further escalation. All other reports follow this procedure. # Account Disabled or Suspended Source: https://bunny.net/docs/account/account-suspended Why a bunny.net account becomes disabled or suspended, and how to resolve each state. If you see a message that your account is disabled or suspended, this guide explains the possible reasons and how to resolve them. There are two distinct states. ## Temporarily disabled This is usually a temporary status that you can resolve by addressing the underlying issue. **Common causes:** * **Trial expired:** Trial accounts are automatically disabled when the trial period ends, unless you top up your account. This prevents unexpected charges. * **Negative balance:** If your balance stays negative for around 4 days, the account is disabled until you top up. See [Negative Balance](/docs/billing/negative-balance). **Resolution:** 1. Log in to your account. You'll be guided through recharging or resolving your balance. 2. If you need help, [contact support](https://bunny.net/contact/). ## Suspended A suspension is more serious. It can result from automated risk detection or a manual review following a violation of our [Terms of Service](https://bunny.net/tos) or [Acceptable Use Policy](https://bunny.net/acceptable-use/). **Common causes:** * **Signup risk detected**, based on signals such as: * Use of VPNs, proxies, or anonymized IP addresses during signup * Mismatches between IP address and billing information * Registrations from regions flagged as high-risk by our payment processor * **Policy violations**, such as: * Attempted fraud or abuse of platform resources * Hosting prohibited content or engaging in restricted activities **Resolution:** 1. [Contact support](https://bunny.net/contact/) to request details about the suspension. 2. Be prepared to provide identification, account verification, or additional context to assist the review. Depending on the nature and severity of the issue, some accounts suspended for policy violations may not be reinstated. > [!NOTE] > > Accounts created using anonymizing services such as VPNs or proxies may be permanently restricted. If your account was suspended under these conditions, you may need to create a new account using a different email address and a direct, non-anonymized connection. ## Need help? If you're unsure which state your account is in or what to do next, [contact support](https://bunny.net/contact/). We can help review your account status and determine the right resolution. # API Keys Source: https://bunny.net/docs/account/api-keys Manage your bunny.net API key for programmatic access. Your API key lets you programmatically update your zones and account settings via the [bunny.net API](/docs/api-reference). You can find your API key at [Account > API Key](https://dash.bunny.net/account/api-key). ![API key](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/api-key.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=37f4db4aaf9dcd8f0418f9be4c8c16b6) ## Viewing your API key Your API key is hidden by default. Click the eye icon to reveal it, or the copy icon to copy it to your clipboard. ![Reveal API key](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/reveal-api-key.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=e6fee7302761f60fa0d51f7435a86e60) ## Regenerating your API key If your API key is compromised, click the refresh icon to generate a new one. You'll be asked to confirm before the key is reset. ![Reset API key](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/reset-api-key.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=877a84cbb9bab8ff2301eacad4c52033) > [!WARNING] > > Resetting your API key will disable the old key immediately. Any applications > or scripts using the old key will stop working. ## Keep your API key safe Your API key has full access to your account. Never share it publicly or commit it to version control. > [!NOTE] > > This is your account-level API key. Storage zones, Stream libraries, and > databases each have their own separate API keys for accessing those specific > services. # Appearance Source: https://bunny.net/docs/account/appearance Choose between light and dark mode for the bunny.net dashboard. You can customize the appearance of the bunny.net dashboard by choosing between light and dark mode. ## Change your theme **1. Go to Account Settings** Navigate to [Account Settings](https://dash.bunny.net/account/settings) in your dashboard. **2. Find the Appearance section** Scroll to the **Appearance** section. **3. Select your theme** Click the light or dark mode icon to switch themes. ![Theme selection](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/appearance.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=c2f4181d4eac7c5118d735151f61f47a) Your theme preference is saved automatically and applies across all your dashboard sessions. # Change Password Source: https://bunny.net/docs/account/change-password Update your bunny.net account password. You can change your password at any time from the Account Settings page. ## Change your password **1. Go to Change Password** Navigate to [Account Settings](https://dash.bunny.net/account/settings) and click **Change Password**, or go directly to the [Change Password](https://dash.bunny.net/account/settings/changepassword) page. ![Change password](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/change-password.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=6f92d3a683c35df73a6b09d18a91e280) **2. Enter your new password** Enter your current password, then enter and confirm your new password. Your password must contain: * At least 12 characters * One special character * One lowercase letter * One uppercase letter * One number **3. Save** Click **Save** to update your password. > [!NOTE] > > For additional account security, consider enabling [two-factor > authentication](/docs/account/two-factor-authentication). ## Forgot your password? If you've forgotten your password and can't log in, use the **Forgot Password** link on the [login page](https://dash.bunny.net) to reset it via email. # Close Account Source: https://bunny.net/docs/account/close-account How to close your bunny.net account. If you'd like to stop using bunny.net, you can close your account from the [Account Settings](https://dash.bunny.net/account/settings) page, or go directly to the [Close Account](https://dash.bunny.net/account/settings/close) page. > [!NOTE] > > Thinking about leaving? We'd love to hear from you first. We actively listen > to feedback and build what our users need to scale. If something's missing for > you, there's a good chance we're already working on it. [Get in > touch](https://bunny.net/contact) before you go. ## Before closing your account > [!WARNING] > > Closing your account is permanent and cannot be reversed. All your data will > be deleted and your account balance cannot be refunded. Before proceeding: * Delete all your Pull Zones and Storage Zones * Download any files you want to keep * Export any data or configurations you may need ## Close your account **1. Go to Close Account** Navigate to [Account Settings](https://dash.bunny.net/account/settings) and click **Close My Account**, or go directly to the [Close Account](https://dash.bunny.net/account/settings/close) page. ![Close account section](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/close-account.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=50f15daff4051b2e543f2fdecb5e2790) **2. Select a reason** Select a reason for closing your account. This feedback helps us improve the service. **3. Confirm closure** Click **Close My Account Forever** to complete the account closure. # Data Processing Agreement Source: https://bunny.net/docs/account/data-processing-agreement View and accept the GDPR data processing agreement. The [Data Processing Agreement](https://dash.bunny.net/account/dpa) page lets you view, accept, and download the GDPR data processing agreement for your account. ## Accepting the agreement The agreement is pre-filled with your account details. Review the terms and click **Accept** to sign the agreement. ## Downloading the agreement Once accepted, you can download a copy of the signed agreement for your records. ![Data Processing Agreement](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/data-processing-agreement.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=3dd8186e24010fa321c02f665d9a5277) # DMCA Procedure Source: https://bunny.net/docs/account/dmca-procedure How bunny.net handles DMCA and copyright takedown requests for customer content. bunny.net takes DMCA and copyright requests seriously, protecting intellectual property rights while giving customers a fair chance to respond. This page outlines how we handle DMCA and copyright notices related to customer content. ## Receiving and verifying the report When we receive a DMCA or copyright report, we first verify its authenticity. Our team reviews the report to confirm it meets the legal requirements of a valid claim, ensuring it is not fraudulent and that it pertains to content hosted on our platform. ## Notifying the customer Once we verify a claim, we notify the customer responsible for the content. Our support team sends an official notice by email, and it is also available in your dashboard. The notice includes the details of the report and clear instructions on the next steps. Customers have **48 hours** to respond from the time the email is sent, with two options: * **Option 1: Remove the content.** If you believe the claim is valid, remove the infringing content from your account. In the dashboard, open the Abuse Case to verify the content has been removed. Once confirmed, the case is marked resolved. * **Option 2: File a counter-claim.** If you believe the report is incorrect or invalid, file a counter-claim within the same 48-hour window. A counter-claim is a legal statement contesting the validity of the request, and it triggers the next steps in the resolution process. ## Blocking the content If no action is taken within the initial 48 hours (the content is neither removed nor a counter-claim filed), the content is blocked and the Pull Zone may be suspended. The content stays inaccessible to the public until the matter is resolved. When a counter-claim is submitted, the content stays blocked while the reporter is notified. The reporter has **10 to 14 days** to respond or initiate legal action. If they don't within that window, we unblock the content after the 14-day period. ## Illegal or highly sensitive content Some content types are treated differently from standard claims: * **Illegal content** (such as phishing or malware): We block it immediately. The DMCA process does not apply, and the content remains blocked pending further investigation or legal action. * **Sports streams:** Because of copyright and broadcasting rights, we immediately block content reported as an unlicensed sports stream. To unblock it, you must provide proof that you are licensed to stream the content. Without proof, it remains blocked. ## Restoring content after a takedown Once a valid takedown has occurred, you may not restore or re-upload the removed content, whether under the same name or a different one. If content is repeatedly re-uploaded after being taken down, we will issue further notices and require its removal again. Continued re-uploads may result in suspension of the entire account, to ensure compliance and protect intellectual property rights. ## After a report is filed Once a report is filed and the content is blocked or a counter-claim is submitted, the next steps are governed by legal requirements. If the reporter does not respond or take legal action within the 10 to 14 day window after a counter-claim, the content is unblocked and restored. If legal action is initiated, the content remains blocked until the dispute is resolved. # Billing Details Source: https://bunny.net/docs/account/index Manage your billing details, billing address, and company information. The [Account Settings](https://dash.bunny.net/account/settings) page lets you manage your personal information, billing address, and company details. ## Personal details Update your basic account information: * **First name** (required) * **Last name** (required) * **Email** (required) - Your primary login email * **Abuse report email** - Separate email for receiving abuse reports * **Billing email** - Separate email for invoices and billing updates > [!NOTE] > > If you configure a billing email, invoices will only be sent to that address > and no longer to your main email. ![Personal details form](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/account-settings-personal-details.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=9dde77396240616fecef5b764869e003) ## Billing address Your billing address appears on invoices. All fields are required: * **Street address** * **City** * **ZIP code** * **Country** ![Billing details form](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/account-settings-billing-details.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=11bacb1e866ebf8413f724008b2c7b19) ### Address verification When you enter or update your billing address, we verify it against an international address database before it is saved. This keeps your invoices valid and ensures we apply the correct [tax rate](/docs/billing/taxation). We check that the address is real and that its parts are consistent with each other. If an address cannot be verified, it is not saved and you will see an error describing what did not match. > [!NOTE] > > If your address is correct but still cannot be verified, [contact > support](https://bunny.net/contact/) and we will review it manually. ## Company Optional fields for business accounts: * **Company name** - Appears on invoices * **VAT number** - For EU VAT-registered businesses ![Company details form](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/account-settings-company-details.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=73e5b64d9a15aba50f081c58c4f6a70a) After making changes, click **Update Account Details** to save. ## When billing details changes appear on your invoice Changes to your billing details apply from the next billing cycle, not the current one. The invoice for the current month still shows your previous details. ### Why At the start of each month, we save your billing details and use them for all usage in that month. The invoice for that month, issued during the following month, shows those details. A change made during the month doesn't affect the billing details already in use, so it applies from the next cycle. ### Example You update your billing address on 15 July: | Date | What happens | Details used | | - | - | - | | 15 July | You save the new billing address | — | | 1 August | August cycle starts with your new details | New address | | August | Invoice for July usage | Old address | | September | Invoice for August usage | New address | ### Quick reference | You change your details in… | First invoice with the new details | | - | - | | July | September | | August | October | | September | November | # Integrations Source: https://bunny.net/docs/account/integrations Manage connected GitHub accounts and linked scripts. The [Integrations](https://dash.bunny.net/account/integrations) page shows your connected GitHub accounts and any scripts linked to them. ![Integrations](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/integrations.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=f9cface9d16c6df3242f1f38381e2801) ## Connected GitHub accounts When you connect a GitHub account, you can deploy Edge Scripts directly from your repositories. Each connected account shows the scripts that are linked to it. ## Viewing connected scripts Click on a connected GitHub account to see the scripts linked to it. ## Unlinking a GitHub account To disconnect a GitHub account, click on it and select **Unlink**. This removes the connection between your bunny.net account and GitHub. > [!NOTE] > > Unlinking a GitHub account does not delete your Edge Scripts. The scripts will > remain but will no longer sync with your repository. # Add a Team Member Source: https://bunny.net/docs/account/team-management/add-team-member Invite new users to your bunny.net account with customized permissions. You can add team members to your bunny.net account, giving them their own login credentials and specific permissions to access parts of your dashboard. ## Prerequisites * You must be the account owner or have the **Manage users** permission ## Add a new team member **1. Navigate to Manage Team** Go to [Account > Manage Team](https://dash.bunny.net/account/users) in your dashboard. **2. Click Add Team Member** Click the **Add New Team Member** button. If this is your first team member, you'll see a **Create Your First Team Member** button. ![Create First Team Member](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/team-management/add-team-member.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=d60cf0be949cf54f0eafcd104dd349bc) **3. Enter login details** Fill in the required fields: * **First Name** - The team member's first name * **Last Name** - The team member's last name * **Email** - The email address they'll use to log in A temporary password will be automatically generated. Make sure to copy and share this securely with the team member. **4. Select permissions** Choose which areas of the dashboard the team member can access: * **Manage zones** - CDN pull zones, storage zones, DNS zones, and Stream libraries * **Billing & legal** - Billing information, invoices, and payment methods * **Support tickets** - Create and manage support tickets * **Abuse center** - View and respond to abuse cases * **Manage users** - Add, edit, and remove other team members **5. Save** Click **Save** to create the team member account. ## After adding a team member Once you've added a team member: 1. Share the temporary password with them securely 2. They can log in at [dash.bunny.net](https://dash.bunny.net) using their email and the temporary password 3. They should change their password after their first login > [!NOTE] > > Team members will only see menu items and features they have permissions for. > Items outside their permissions are hidden from their dashboard view. # Team Management Source: https://bunny.net/docs/account/team-management/index Add team members to your bunny.net account and manage their permissions. Team Management allows you to invite additional users to your bunny.net account, each with their own login credentials and customizable permissions. This is useful for organizations where multiple people need access to different parts of the dashboard. > **[Add a Team Member](/docs/account/team-management/add-team-member)** ## Key features * **Individual logins** - Each team member has their own email and password * **Granular permissions** - Control exactly what each team member can access * **Centralized management** - View and manage all team members from one place ## Managing your team You can manage your team from the [Manage Team](https://dash.bunny.net/account/users) page in your dashboard. Navigate to **Account > Manage Team** to: * View all team members and their permissions * Add new team members * Edit existing team member permissions * Remove team members ## Available permissions When adding or editing team members, you can assign the following permissions: | Permission | Description | | - | - | | Manage zones | Access to CDN pull zones, storage zones, DNS zones, and Stream libraries | | Billing & legal | Access to billing information, invoices, and payment methods | | Support tickets | Ability to create and manage support tickets | | Abuse center | Access to view and respond to abuse cases | | Manage users | Ability to add, edit, and remove other team members | > [!NOTE] > > Team members cannot access features they don't have permissions for. The corresponding menu items will not be visible in their dashboard. # Team Permissions Source: https://bunny.net/docs/account/team-management/permissions Understand the available permissions for team members in your bunny.net account. When you add or edit a team member, you can assign specific permissions that control what they can access in the bunny.net dashboard. Team members only see menu items and features they have permissions for. ## Available permissions | Permission | Description | | - | - | | Manage zones | Access to CDN pull zones, storage zones, DNS zones, and Stream libraries | | Billing & legal | Access to billing information, invoices, payment methods, and legal documents | | Support tickets | Ability to create, view, and manage support tickets | | Abuse center | Access to view and respond to abuse cases | | Manage users | Ability to add, edit, and remove other team members | ## Permission details ### Manage zones Team members with this permission can: * Create, configure, and delete CDN pull zones * Manage storage zones and upload files * Configure DNS zones and records * Access Stream video libraries and manage video content * Configure Edge Scripting * Manage Shield settings This is the primary permission for team members who need to manage your content delivery infrastructure. ### Billing & legal Team members with this permission can: * View current balance and usage * Access billing history and download invoices * Manage payment methods and auto-recharge settings * View and accept legal agreements (such as the Data Processing Agreement) > [!WARNING] > > Only grant this permission to team members who need access to financial information and payment settings. ### Support tickets Team members with this permission can: * Create new support tickets * View and respond to existing tickets * Access support ticket history This is useful for team members who may need to contact bunny.net support on behalf of your organization. ### Abuse center Team members with this permission can: * View active abuse cases * Respond to abuse notifications * Access abuse case history This permission is useful for team members responsible for content moderation or compliance. ### Manage users Team members with this permission can: * Add new team members * Edit existing team member permissions * Remove team members from the account > [!WARNING] > > Be careful when granting this permission. Team members with Manage users access can modify permissions for other users, including granting themselves additional permissions. ## Permission combinations You can assign any combination of permissions to a team member. Common configurations include: | Role | Recommended permissions | | - | - | | Developer | Manage zones | | Finance/Admin | Billing & legal | | Support lead | Support tickets, Abuse center | | Account manager | All permissions | ## Viewing team member permissions You can see which permissions each team member has from the [Manage Team](https://dash.bunny.net/account/users) overview page. Each team member's row displays their assigned permissions. # Remove a Team Member Source: https://bunny.net/docs/account/team-management/remove-team-member Remove users from your bunny.net account or update their permissions. You can remove team members from your account or edit their permissions at any time from the Manage Team page. ## Prerequisites * You must be the account owner or have the **Manage users** permission ## Remove a team member **1. Navigate to Manage Team** Go to [Account > Manage Team](https://dash.bunny.net/account/users) in your dashboard. **2. Find the team member** Locate the team member you want to remove in the list. You can see each member's name, email, and current permissions. **3. Select Delete from the action menu** Click the **...** (three dots) action menu on the team member's row, and click **Delete** from the dropdown menu. ![Delete](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/team-management/remove-team-member.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=5bdf57cb5db8e3f587d5b2868465269c) **4. Confirm removal** Confirm the deletion when prompted. > [!WARNING] > > Removing a team member immediately revokes their access. They will no longer > be able to log in to your account. ## Edit team member permissions If you need to change a team member's permissions rather than remove them entirely: **1. Navigate to Manage Team** Go to [Account > Manage Team](https://dash.bunny.net/account/users) in your dashboard. **2. Find the team member** Locate the team member whose permissions you want to update. **3. Select Edit from the action menu** Click the **...** (three dots) action menu on the team member's row, and click **Edit** from the dropdown menu. ![Edit](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/team-management/edit-team-member.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=a9c235141d86e7eb56dd06c2748c7ca9) **4. Update permissions** Check or uncheck the permissions as needed: * **Manage zones** * **Billing & legal** * **Support tickets** * **Abuse center** * **Manage users** **5. Save changes** Click **Save User** to apply the updated permissions. > [!NOTE] > > Permission changes take effect immediately. The team member's dashboard view > will update to reflect their new access level on their next page load. # Two-Factor Authentication Source: https://bunny.net/docs/account/two-factor-authentication Add an extra layer of security to your bunny.net account. Two-factor authentication (2FA) adds an extra layer of security to your account. When enabled, you'll need to enter a PIN code from your authentication app each time you log in. ## Enable two-factor authentication **1. Go to Two-Factor Setup** Navigate to [Account Settings](https://dash.bunny.net/account/settings) and click **Enable Two-Factor Authentication**, or go directly to the [Two-Factor Setup](https://dash.bunny.net/account/settings/twofactorsetup) page. **2. Scan the QR code** Open your authenticator app (such as Google Authenticator, Authy, or 1Password) and scan the QR code displayed. **3. Enter the verification code** Enter the 6-digit code from your authenticator app to validate the setup. **4. Save your backup codes** After validation, you'll receive backup codes. Store these in a safe place - you can use them to access your account if you lose access to your authenticator app. ![Two-factor authentication settings](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/two-factor-authentication.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=bf762a657eccfe919b9599abdd76bde0) ## Backup codes Backup codes are one-time use codes that let you log in if you lose access to your authenticator app. Each code can only be used once. > [!WARNING] > > Store your backup codes securely. If you lose both your authenticator app and > your backup codes, you'll need to contact support to regain access to your > account. ## Supported authenticator apps Any TOTP-compatible authenticator app works with bunny.net, including: * Google Authenticator * Authy * 1Password * Microsoft Authenticator * Bitwarden ## Disable two-factor authentication To disable 2FA, return to Account Settings and click **Disable Two-Factor Authentication**. You'll need to enter a code from your authenticator app to confirm. > [!WARNING] > > Disabling two-factor authentication reduces the security of your account. Only > disable it if necessary. # Authentication Source: https://bunny.net/docs/api-reference/authentication Learn how to authenticate requests to the bunny.net APIs. Send your API key in the `AccessKey` header with every request. ```bash theme={null} curl --request GET \ --url https://api.bunny.net/dnszone \ --header 'AccessKey: YOUR_API_KEY' ``` ## Find your API key Your account has one API key, and it covers account-level work such as managing pull zones, DNS zones, and other resources. You can view it at any time in the [dashboard](https://dash.bunny.net) under **Account** > **API**. > [!NOTE] > > We're working on scoped API keys. You'll be able to limit an API key to specific products and actions. ## Product credentials Some APIs expect a credential scoped to a single resource instead of your account API key: | API | Credential | | - | - | | Bunny Stream | The [video library API key](/docs/stream/authentication) | | Bunny Storage | The [storage zone password](/docs/storage/http) | | Bunny Database | A [database auth token](/docs/database/connect/authorization#access-tokens) | Managing video libraries, storage zones, and databases themselves uses your account API key. > [!WARNING] > > Anyone holding your API key can act on your account. Keep it in a secret store or environment variable, and leave it out of client-side code and public > repositories. # Logging API Reference Source: https://bunny.net/docs/api-reference/cdn-logging/index Access raw CDN request logs for your pull zones via HTTP. The Logging API provides access to raw request logs for all pull zones with logging enabled. Logs appear in near real-time and are retained for 3 days. ## Base URL ``` https://logging.bunnycdn.com ``` ## Authentication Authenticate using the `AccessKey` header or a bearer JWT: ```bash theme={null} curl --request GET \ --url https://logging.bunnycdn.com/v2/pullzones/{pullZoneId}/logs \ --header 'AccessKey: YOUR_API_KEY' ``` ```bash theme={null} curl --request GET \ --url https://logging.bunnycdn.com/v2/pullzones/{pullZoneId}/logs \ --header 'Authorization: Bearer YOUR_JWT' ``` > [!NOTE] > > Find your API key in the [account > settings](https://dash.bunny.net/account/settings) under **API Keys**. ## API versions Two versions of the API are available: | Version | Format | Description | | - | - | - | | [v2](/docs/api-reference/cdn-logging/logging-v2/query-cdn-access-logs-for-a-pull-zone) | JSON | Structured JSON with rich filtering, pagination, and per-field search. **Recommended.** | | [v1](/docs/api-reference/cdn-logging/logging-v1/query-logs-legacy) | Pipe-delimited | Streams a raw pipe-delimited file for a given day. Preserved for existing integrations. | ## Cache status values Both API versions return the following cache status values: | Value | Description | | - | - | | `HIT` | Response served directly from cache | | `MISS` | Response not in cache; fetched from origin | | `BYPASS` | Cache was skipped (e.g. due to request headers, cookies, or configuration) | | `REVALIDATED` | Cached response was validated with origin and reused | | `STALE` | Stale cached response served (typically due to origin being unavailable or slow) | | `UPDATING` | Stale content served while a background cache update is in progress | | `-` | No cache interaction (e.g. non-cacheable methods like `POST`) | ## Rate limits Both API versions enforce a per-pull-zone rate limit of **30 requests per 10 seconds**. Exceeding this returns a `429` response. ## Error codes | HTTP Status | Code | Description | | - | - | - | | 400 | `invalid_request` | One or more query parameters failed validation | | 401 | `unauthorized` | No authentication credentials provided | | 403 | `forbidden` | Credentials are invalid, or pull zone is suspended/disabled | | 404 | `logging_disabled` | Logging is not enabled for this pull zone | | 429 | `rate_limited` | Per-pull-zone rate limit exceeded | | 500 | `internal_error` | Unexpected server error | # Query logs (legacy) Source: https://bunny.net/docs/api-reference/cdn-logging/logging-v1/query-logs-legacy https://logging.bunnycdn.com/docs/all/swagger.json get /{date}/{pullZoneId}.log # Query CDN access logs for a pull zone. Source: https://bunny.net/docs/api-reference/cdn-logging/logging-v2/query-cdn-access-logs-for-a-pull-zone https://logging.bunnycdn.com/docs/all/swagger.json get /v2/pullzones/{pullZoneId}/logs Authenticate with either an `Authorization` bearer JWT or an `AccessKey` header. Filter pushdown happens in ClickHouse where possible; `country` and free-text `search` are applied in-process after fetch. # Get affiliate details Source: https://bunny.net/docs/api-reference/core/affiliate/get-affiliate-details https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /billing/affiliate # List API Keys Source: https://bunny.net/docs/api-reference/core/api-keys/list-api-keys https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /apikey # Get useraudit Source: https://bunny.net/docs/api-reference/core/auditlog/get-useraudit https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /user/audit/{date} # Download Payment Request Invoice PDF Source: https://bunny.net/docs/api-reference/core/billing/download-payment-request-invoice-pdf https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /billing/payment-request-invoice/{id}/pdf # Get Billing Details Source: https://bunny.net/docs/api-reference/core/billing/get-billing-details https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /billing Get the billing status details # Get Billing Summary Source: https://bunny.net/docs/api-reference/core/billing/get-billing-summary https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /billing/summary # Get Billing Summary Document Source: https://bunny.net/docs/api-reference/core/billing/get-billing-summary-document https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /billing/summary/{billingRecordId}/pdf # Get Pending Payment Requests Source: https://bunny.net/docs/api-reference/core/billing/get-pending-payment-requests https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /billing/payment-requests # Get Country List Source: https://bunny.net/docs/api-reference/core/countries/get-country-list https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /country # Add DNS Record Source: https://bunny.net/docs/api-reference/core/dns-zone/add-dns-record https://core-api-public-docs.b-cdn.net/docs/v3/public.json put /dnszone/{zoneId}/records # Add DNS Zone Source: https://bunny.net/docs/api-reference/core/dns-zone/add-dns-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone # Check the DNS zone availability Source: https://bunny.net/docs/api-reference/core/dns-zone/check-the-dns-zone-availability https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/checkavailability # Delete DNS Record Source: https://bunny.net/docs/api-reference/core/dns-zone/delete-dns-record https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /dnszone/{zoneId}/records/{id} # Delete DNS Zone Source: https://bunny.net/docs/api-reference/core/dns-zone/delete-dns-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /dnszone/{id} # Disable DNSSEC on a DNS Zone Source: https://bunny.net/docs/api-reference/core/dns-zone/disable-dnssec-on-a-dns-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /dnszone/{id}/dnssec # Enable DNSSEC on a DNS Zone Source: https://bunny.net/docs/api-reference/core/dns-zone/enable-dnssec-on-a-dns-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/{id}/dnssec # Get DNS Query Statistics Source: https://bunny.net/docs/api-reference/core/dns-zone/get-dns-query-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /dnszone/{id}/statistics # Get DNS Zone Source: https://bunny.net/docs/api-reference/core/dns-zone/get-dns-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /dnszone/{id} # Get dnszone export Source: https://bunny.net/docs/api-reference/core/dns-zone/get-dnszone-export https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /dnszone/{id}/export # Get the latest DNS record scan result for a DNS Zone Source: https://bunny.net/docs/api-reference/core/dns-zone/get-the-latest-dns-record-scan-result-for-a-dns-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /dnszone/{zoneId}/records/scan # Import DNS Records Source: https://bunny.net/docs/api-reference/core/dns-zone/import-dns-records https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/{zoneId}/import # Issue new wildcard certificate Source: https://bunny.net/docs/api-reference/core/dns-zone/issue-new-wildcard-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/{zoneId}/certificate/issue # List DNS Zone Records Source: https://bunny.net/docs/api-reference/core/dns-zone/list-dns-zone-records https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /dnszone/{zoneId}/records # List DNS Zones Source: https://bunny.net/docs/api-reference/core/dns-zone/list-dns-zones https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /dnszone # Trigger a background scan for pre-existing DNS records. Can use ZoneId for existing zones or Domain for pre-zone creation scenarios. Source: https://bunny.net/docs/api-reference/core/dns-zone/trigger-a-background-scan-for-pre-existing-dns-records-can-use-zoneid-for-existing-zones-or-domain-for-pre-zone-creation-scenarios https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/records/scan # Update DNS Record Source: https://bunny.net/docs/api-reference/core/dns-zone/update-dns-record https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/{zoneId}/records/{id} # Update DNS Zones Source: https://bunny.net/docs/api-reference/core/dns-zone/update-dns-zones https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /dnszone/{id} # Errors Source: https://bunny.net/docs/api-reference/core/errors Standard HTTP error codes returned by the bunny.net API. This section outlines the standard HTTP error codes returned by the API. These errors provide insight into why a request may have failed and how to address the issue. Each error code corresponds to a specific type of problem encountered during the processing of the request. ## Error response When an error occurs, the API returns a JSON object with details about the error: ```json theme={null} { "ErrorKey": "pullZone.not_found", "Field": "PullZone", "Message": "The requested Pull Zone was not found" } ``` | Field | Description | | - | - | | `ErrorKey` | A machine-readable error identifier | | `Field` | The field or resource related to the error | | `Message` | A human-readable description of the error | ## Status codes | Code | Description | | - | - | | `400` | The request did not pass validation. This may include issues with the request model, body, or parameters. | | `401` | Authorization for the request has failed. | | `403` | The action is forbidden for the user. This may include restrictions on the URL or the HTTP method being used. | | `404` | The requested entity could not be found. | | `429` | Too many requests; the rate limit has been exceeded. | | `500` | An internal server error occurred. | # Core API Reference Source: https://bunny.net/docs/api-reference/core/index Manage your bunny.net account, pull zones, storage zones, DNS, and more. The Core Platform API provides a RESTful interface for managing your bunny.net account and all associated resources. Create and configure pull zones, manage storage, set up DNS, and access billing and statistics. ## Base URL ``` https://api.bunny.net ``` ## Authentication Authenticate using the `AccessKey` header with your account API key: ```bash theme={null} curl --request GET \ --url https://api.bunny.net/pullzone \ --header 'AccessKey: YOUR_API_KEY' ``` > [!NOTE] > > Find your API key in the [account > settings](https://dash.bunny.net/account/settings) under **API Keys**. ## Resources The Core Platform API provides access to the following resources: | Resource | Description | | - | - | | Pull Zones | Create and configure CDN pull zones for content delivery | | Storage Zones | Manage edge storage zones for file hosting | | DNS Zones | Configure DNS records and zone settings | | Stream Video Libraries | Create and manage video libraries | | Statistics | Access bandwidth, requests, and cache statistics | | Billing | View billing summaries and usage details | | Purge | Purge cached content from the CDN | | API Keys | Manage API keys for programmatic access | | Countries | Retrieve country lists for geo-blocking | | Regions | List available edge regions | ## Product-Specific APIs For managing content within specific products, use the dedicated APIs: > **[Stream API](/docs/api-reference/stream)** > > Upload and manage videos in your libraries > **[Storage API](/docs/api-reference/storage)** > > Upload and download files in storage zones > **[Shield API](/docs/api-reference/shield)** > > Configure WAF rules and security settings > **[Scripting API](/docs/api-reference/scripting)** > > Deploy and manage edge scripts # Add a new origin group to a Load Balancer profile Source: https://bunny.net/docs/api-reference/core/load-balancer/add-a-new-origin-group-to-a-load-balancer-profile https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /loadbalancer/{loadBalancerId}/origingroup # Add a new origin to an origin group Source: https://bunny.net/docs/api-reference/core/load-balancer/add-a-new-origin-to-an-origin-group https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /loadbalancer/{loadBalancerId}/origingroup/{originGroupId}/origin # Create a new Load Balancer profile Source: https://bunny.net/docs/api-reference/core/load-balancer/create-a-new-load-balancer-profile https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /loadbalancer # Delete a Load Balancer profile Source: https://bunny.net/docs/api-reference/core/load-balancer/delete-a-load-balancer-profile https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /loadbalancer/{loadBalancerId} # Delete an origin from an origin group Source: https://bunny.net/docs/api-reference/core/load-balancer/delete-an-origin-from-an-origin-group https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /loadbalancer/{loadBalancerId}/origingroup/{originGroupId}/origin/{originId} # Delete an origin group from a Load Balancer profile Source: https://bunny.net/docs/api-reference/core/load-balancer/delete-an-origin-group-from-a-load-balancer-profile https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /loadbalancer/{loadBalancerId}/origingroup/{id} # Get a Load Balancer profile Source: https://bunny.net/docs/api-reference/core/load-balancer/get-a-load-balancer-profile https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /loadbalancer/{loadBalancerId} # Get current-period request usage and pricing for a Load Balancer Source: https://bunny.net/docs/api-reference/core/load-balancer/get-current-period-request-usage-and-pricing-for-a-load-balancer https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /loadbalancer/{loadBalancerId}/usage # Get Load Balancer statistics Source: https://bunny.net/docs/api-reference/core/load-balancer/get-load-balancer-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /loadbalancer/{loadBalancerId}/statistics # Get statistics roll-ups for all Load Balancers on the account Source: https://bunny.net/docs/api-reference/core/load-balancer/get-statistics-roll-ups-for-all-load-balancers-on-the-account https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /loadbalancer/statistics # Get the list of Load Balancer profiles Source: https://bunny.net/docs/api-reference/core/load-balancer/get-the-list-of-load-balancer-profiles https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /loadbalancer # Get the pull zones attached to a Load Balancer Source: https://bunny.net/docs/api-reference/core/load-balancer/get-the-pull-zones-attached-to-a-load-balancer https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /loadbalancer/{loadBalancerId}/pullzones # Update a Load Balancer profile Source: https://bunny.net/docs/api-reference/core/load-balancer/update-a-load-balancer-profile https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /loadbalancer/{loadBalancerId} # Update an existing origin Source: https://bunny.net/docs/api-reference/core/load-balancer/update-an-existing-origin https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /loadbalancer/{loadBalancerId}/origingroup/{originGroupId}/origin/{originId} # Update an origin group in a Load Balancer profile Source: https://bunny.net/docs/api-reference/core/load-balancer/update-an-origin-group-in-a-load-balancer-profile https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /loadbalancer/{loadBalancerId}/origingroup/{id} # Add Allowed Referer Source: https://bunny.net/docs/api-reference/core/pull-zone/add-allowed-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/addAllowedReferrer # Add Blocked IP Source: https://bunny.net/docs/api-reference/core/pull-zone/add-blocked-ip https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/addBlockedIp # Add Blocked Referer Source: https://bunny.net/docs/api-reference/core/pull-zone/add-blocked-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/addBlockedReferrer # Add Custom Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/add-custom-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/addCertificate # Add Custom Hostname Source: https://bunny.net/docs/api-reference/core/pull-zone/add-custom-hostname https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/addHostname # Add Pull Zone Source: https://bunny.net/docs/api-reference/core/pull-zone/add-pull-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone # Add/Update Edge Rule Source: https://bunny.net/docs/api-reference/core/pull-zone/addupdate-edge-rule https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{pullZoneId}/edgerules/addOrUpdate # Change hostname private key type Source: https://bunny.net/docs/api-reference/core/pull-zone/change-hostname-private-key-type https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/updatePrivateKeyType # Check the pull zone availability Source: https://bunny.net/docs/api-reference/core/pull-zone/check-the-pull-zone-availability https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/checkavailability # Complete External DNS Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/complete-external-dns-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/completeExternalDnsCertificate # Complete External HTTP Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/complete-external-http-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/completeExternalHttpCertificate # Count Pull Zones Source: https://bunny.net/docs/api-reference/core/pull-zone/count-pull-zones https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone/count # Delete Edge Rule Source: https://bunny.net/docs/api-reference/core/pull-zone/delete-edge-rule https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /pullzone/{pullZoneId}/edgerules/{edgeRuleId} # Delete Pull Zone Source: https://bunny.net/docs/api-reference/core/pull-zone/delete-pull-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /pullzone/{id} # Get Pull Zone Source: https://bunny.net/docs/api-reference/core/pull-zone/get-pull-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone/{id} # List Pull Zones Source: https://bunny.net/docs/api-reference/core/pull-zone/list-pull-zones https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone # Load Free Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/load-free-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone/loadFreeCertificate # Purge Cache Source: https://bunny.net/docs/api-reference/core/pull-zone/purge-cache https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/purgeCache # Remove Allowed Referer Source: https://bunny.net/docs/api-reference/core/pull-zone/remove-allowed-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/removeAllowedReferrer # Remove Blocked IP Source: https://bunny.net/docs/api-reference/core/pull-zone/remove-blocked-ip https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/removeBlockedIp # Remove Blocked Referer Source: https://bunny.net/docs/api-reference/core/pull-zone/remove-blocked-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/removeBlockedReferrer # Remove Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/remove-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /pullzone/{id}/removeCertificate # Remove Custom Hostname Source: https://bunny.net/docs/api-reference/core/pull-zone/remove-custom-hostname https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /pullzone/{id}/removeHostname # Request External DNS Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/request-external-dns-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/requestExternalDnsCertificate # Request External HTTP Certificate Source: https://bunny.net/docs/api-reference/core/pull-zone/request-external-http-certificate https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/requestExternalHttpCertificate # Reset Token Key Source: https://bunny.net/docs/api-reference/core/pull-zone/reset-token-key https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/resetSecurityKey # Set Edge Rule Enabled Source: https://bunny.net/docs/api-reference/core/pull-zone/set-edge-rule-enabled https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{pullZoneId}/edgerules/{edgeRuleId}/setEdgeRuleEnabled # Set Force SSL Source: https://bunny.net/docs/api-reference/core/pull-zone/set-force-ssl https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id}/setForceSSL # Update Pull Zone Source: https://bunny.net/docs/api-reference/core/pull-zone/update-pull-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /pullzone/{id} # Purge URL Source: https://bunny.net/docs/api-reference/core/purge/purge-url https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /purge # Region list Source: https://bunny.net/docs/api-reference/core/region/region-list https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /region # Global Search Source: https://bunny.net/docs/api-reference/core/search/global-search https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /search # Statistics API date ranges and granularity Source: https://bunny.net/docs/api-reference/core/statistics-date-ranges The date ranges the Statistics API endpoints accept, what they default to, and when you can request hourly data. Every Statistics endpoint except for [Get DNS Query Statistics](/docs/api-reference/core/dns-zone/get-dns-query-statistics) takes the same three query parameters to shape the data it returns. `dateFrom` and `dateTo` set the range, and `hourly` picks the granularity. This page covers what each one does, what happens when you leave them out, and how far back each endpoint lets you look. ## Granularity and how far back you can go Set `hourly=true` to get one data point per hour. Leave it out, or set `hourly=false`, to get one data point per day. Hourly data covers the last 30 days. A request with `hourly=true` and a `dateFrom` older than 30 days fails with this error: ``` Cannot request hourly statistics older than 30 days. Use daily granularity instead. ``` Daily data covers the last year. A request with a `dateFrom` older than one year fails with this error: ``` Cannot request statistics older than 1 year. ``` Data older than one year isn't available through these endpoints at either granularity. ## Defaults when you omit the dates * `hourly=true` with no dates returns the last 30 days. * `hourly=false`, or no `hourly` parameter, with no dates returns data starting from the same day on the proceeding month. * `dateFrom` on its own sets `dateTo` to now. ## How the API reads your dates The API rounds the range to whole days. `dateFrom` rounds down to the start of its day and `dateTo` rounds up to the end of its day, so you get complete days back even when you pass a time of day. `GET /statistics` also accepts `exactRange=true`, which keeps the hour components of `dateFrom` and `dateTo` as you sent them. It only takes effect together with `hourly=true`. ## Endpoint limits | Endpoint | Route | Default range | Maximum range | Oldest data | | - | - | - | - | - | | [Get Statistics](/docs/api-reference/core/statistics/get-statistics) | `GET /statistics` | Last 30 days (hourly) or last month (daily) | 40 days | 30 days (hourly) or 1 year (daily) | | [Get Statistics](/docs/api-reference/core/statistics/get-statistics) with `loadLatest=true` | `GET /statistics` | None | 1 day | None | | [Get Optimizer Statistics](/docs/api-reference/core/statistics/get-optimizer-statistics) | `GET /pullzone/{pullZoneId}/optimizer/statistics` | Last 30 days (hourly) or last month (daily) | The general rule above | 30 days (hourly) or 1 year (daily) | | [Get Origin Shield Queue Statistics](/docs/api-reference/core/statistics/get-origin-shield-queue-statistics) | `GET /pullzone/{pullZoneId}/originshield/queuestatistics` | Last 30 days (hourly) or last month (daily) | The general rule above | 30 days (hourly) or 1 year (daily) | | [Get SafeHop Statistics](/docs/api-reference/core/statistics/get-safehop-statistics) | `GET /pullzone/{pullZoneId}/safehop/statistics` | Last 30 days (hourly) or last month (daily) | The general rule above | 30 days (hourly) or 1 year (daily) | ## Picking a range For anything within the last 30 days that you want broken down by hour, set `hourly=true`. For anything older than that, and up to a year back, use daily granularity by leaving `hourly` out. # Get optimizer statistics Source: https://bunny.net/docs/api-reference/core/statistics/get-optimizer-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone/{pullZoneId}/optimizer/statistics # Get Origin Shield Queue Statistics Source: https://bunny.net/docs/api-reference/core/statistics/get-origin-shield-queue-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone/{pullZoneId}/originshield/queuestatistics # Get SafeHop Statistics Source: https://bunny.net/docs/api-reference/core/statistics/get-safehop-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /pullzone/{pullZoneId}/safehop/statistics # Get Statistics Source: https://bunny.net/docs/api-reference/core/statistics/get-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /statistics # Add Storage Zone Source: https://bunny.net/docs/api-reference/core/storage-zone/add-storage-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /storagezone # Check the storage zone availability Source: https://bunny.net/docs/api-reference/core/storage-zone/check-the-storage-zone-availability https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /storagezone/checkavailability # Delete Storage Zone Source: https://bunny.net/docs/api-reference/core/storage-zone/delete-storage-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /storagezone/{id} # Get Storage Zone Source: https://bunny.net/docs/api-reference/core/storage-zone/get-storage-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /storagezone/{id} # Get Storage Zone Egress Statistics Source: https://bunny.net/docs/api-reference/core/storage-zone/get-storage-zone-egress-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /storagezone/{id}/statistics/egress # Get Storage Zone Regions Source: https://bunny.net/docs/api-reference/core/storage-zone/get-storage-zone-regions https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /storagezone/regions # Get Storage Zone Statistics Source: https://bunny.net/docs/api-reference/core/storage-zone/get-storage-zone-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /storagezone/{id}/statistics # List Storage Zones Source: https://bunny.net/docs/api-reference/core/storage-zone/list-storage-zones https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /storagezone # Reset Password Source: https://bunny.net/docs/api-reference/core/storage-zone/reset-password https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /storagezone/{id}/resetPassword # Reset Read-Only Password Source: https://bunny.net/docs/api-reference/core/storage-zone/reset-read-only-password https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /storagezone/resetReadOnlyPassword # Update Storage Zone Source: https://bunny.net/docs/api-reference/core/storage-zone/update-storage-zone https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /storagezone/{id} # Add Allowed Referer Source: https://bunny.net/docs/api-reference/core/stream-video-library/add-allowed-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id}/addAllowedReferrer # Add Blocked Referer Source: https://bunny.net/docs/api-reference/core/stream-video-library/add-blocked-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id}/addBlockedReferrer # Add Video Library Source: https://bunny.net/docs/api-reference/core/stream-video-library/add-video-library https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary # Add Watermark Source: https://bunny.net/docs/api-reference/core/stream-video-library/add-watermark https://core-api-public-docs.b-cdn.net/docs/v3/public.json put /videolibrary/{id}/watermark # Delete Video Library Source: https://bunny.net/docs/api-reference/core/stream-video-library/delete-video-library https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /videolibrary/{id} # Delete Watermark Source: https://bunny.net/docs/api-reference/core/stream-video-library/delete-watermark https://core-api-public-docs.b-cdn.net/docs/v3/public.json delete /videolibrary/{id}/watermark # Get Languages Source: https://bunny.net/docs/api-reference/core/stream-video-library/get-languages https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /videolibrary/languages # Get Video Library Source: https://bunny.net/docs/api-reference/core/stream-video-library/get-video-library https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /videolibrary/{id} # Get Video Library DRM Statistics Source: https://bunny.net/docs/api-reference/core/stream-video-library/get-video-library-drm-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /videolibrary/{id}/drm/statistics # Get Video Library Transcribing Statistics Source: https://bunny.net/docs/api-reference/core/stream-video-library/get-video-library-transcribing-statistics https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /videolibrary/{id}/transcribing/statistics # List Video Libraries Source: https://bunny.net/docs/api-reference/core/stream-video-library/list-video-libraries https://core-api-public-docs.b-cdn.net/docs/v3/public.json get /videolibrary # Remove Allowed Referer Source: https://bunny.net/docs/api-reference/core/stream-video-library/remove-allowed-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id}/removeAllowedReferrer # Remove Blocked Referer Source: https://bunny.net/docs/api-reference/core/stream-video-library/remove-blocked-referer https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id}/removeBlockedReferrer # Reset API Key Source: https://bunny.net/docs/api-reference/core/stream-video-library/reset-api-key https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id}/resetApiKey # Reset Read Only API Key Source: https://bunny.net/docs/api-reference/core/stream-video-library/reset-read-only-api-key https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id}/resetReadOnlyApiKey # Update Video Library Source: https://bunny.net/docs/api-reference/core/stream-video-library/update-video-library https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /videolibrary/{id} # Close the account Source: https://bunny.net/docs/api-reference/core/user/close-the-account https://core-api-public-docs.b-cdn.net/docs/v3/public.json post /user/closeaccount Close the current user account # Introduction Source: https://bunny.net/docs/api-reference/index Learn about the bunny.net APIs and how to integrate with the platform. ![bunny.net API](https://mintcdn.com/bunnynet-cb9733c2/lCP0r4EEydbwexRJ/images/api/bunny-api.png?fit=max\&auto=format\&n=lCP0r4EEydbwexRJ\&q=85\&s=704a18e079b866f90e5430bb10debee1) The bunny.net APIs let you manage your account and products over HTTP. Create a pull zone from a deploy script, upload files from CI, fetch logs, and more. > **[Quickstart](/docs/api-reference/quickstart)** > > Get an API key and make your first request Every request carries an `AccessKey` header. Most APIs take your account API key, and Stream and Edge Storage each use their own credentials. The [authentication guide](/docs/api-reference/authentication) covers which key goes where. ## Core Platform API The [Core Platform API](/docs/api-reference/core) manages your account and the resources inside it: * Pull Zones (CDN) * Storage Zones * DNS Zones * Stream Video Libraries * Statistics and billing ```bash theme={null} https://api.bunny.net ``` ## Product APIs Each product has its own API for content and product-specific operations. Some share the Core Platform base URL, and others run on their own host. | Product | Description | Base URL | | - | - | - | | [Stream](/docs/api-reference/stream) | Manage video libraries, collections, and videos | `https://video.bunnycdn.com` | | [Edge Storage](/docs/api-reference/storage) | Upload, download, and manage files in storage zones | `https://{region}.storage.bunnycdn.com` | | [Shield](/docs/api-reference/shield) | Configure WAF rules, rate limiting, and DDoS protection | `https://api.bunny.net` | | [Scripting](/docs/api-reference/scripting) | Deploy and manage edge scripts | `https://api.bunny.net` | | [Magic Containers](/docs/api-reference/magic-containers) | Manage container applications and deployments | `https://api.bunny.net/mc` | | [Origin Errors](/docs/api-reference/origin-errors) | Retrieve origin error logs for pull zones | `https://cdn-origin-logging.bunny.net` | | [CDN Logging](/docs/api-reference/cdn-logging) | Retrieve raw request logs for pull zones | `https://logging.bunnycdn.com` | | [Partner Portal](/docs/api-reference/partner) | Manage customers, pricing, and billing as a partner | `https://api.bunny.net` | # Add Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/add-application https://api-mc.opsbunny.net/docs/public/swagger.json post /apps Creates a new application with the specified configuration including containers, volumes, region settings, and autoscaling. # Delete Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/delete-application https://api-mc.opsbunny.net/docs/public/swagger.json delete /apps/{appId} Marks the application for deletion and enqueues cleanup of all associated resources. Returns immediately; deletion is processed asynchronously. # Deploy Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/deploy-application https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/deploy Deploys an application, making it active and running. # Get application Source: https://bunny.net/docs/api-reference/magic-containers/applications/get-application https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId} # Get Application Overview Source: https://bunny.net/docs/api-reference/magic-containers/applications/get-application-overview https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/overview Retrieves comprehensive status overview for an application including latency, CPU/RAM usage, active instances, regions, and cost information. # Get Application Statistics Source: https://bunny.net/docs/api-reference/magic-containers/applications/get-application-statistics https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/statistics Retrieves historical statistics for an application including CPU, RAM, traffic, latency, and volume usage over a specified time period. # Get Application Usage Summary Source: https://bunny.net/docs/api-reference/magic-containers/applications/get-application-usage-summary https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/summary Retrieves usage summary for an application including latency, volume size, monthly cost, and status information. # List Applications Source: https://bunny.net/docs/api-reference/magic-containers/applications/list-applications https://api-mc.opsbunny.net/docs/public/swagger.json get /apps Lists all applications for the authenticated user with their current status. # Patch Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/patch-application https://api-mc.opsbunny.net/docs/public/swagger.json patch /apps/{appId} Partially updates an existing application using JSON Merge Patch semantics. Only provided fields will be updated; existing fields not included in the request will remain unchanged. For arrays (containers, volumes, endpoints), items with matching IDs will be updated, items without IDs will be added as new, and items not included will be deleted. # Restart Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/restart-application https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/restart Triggers a restart of all pods for the specified application. # Undeploy Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/undeploy-application https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/undeploy Undeploys an application, stopping all running instances. # Update Application Source: https://bunny.net/docs/api-reference/magic-containers/applications/update-application https://api-mc.opsbunny.net/docs/public/swagger.json put /apps/{appId} Updates an existing application with full replacement of all configuration fields. # Get Application Autoscaling Source: https://bunny.net/docs/api-reference/magic-containers/autoscalingsettings/get-application-autoscaling https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/autoscaling Retrieves the current autoscaling settings for an application, including minimum and maximum replica counts. # Update Application Autoscaling Source: https://bunny.net/docs/api-reference/magic-containers/autoscalingsettings/update-application-autoscaling https://api-mc.opsbunny.net/docs/public/swagger.json put /apps/{appId}/autoscaling Updates the autoscaling settings for an application, including minimum and maximum replica counts. # Add container registry Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/add-container-registry https://api-mc.opsbunny.net/docs/public/swagger.json post /registries Add a container registry for user. # Delete Container Registry Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/delete-container-registry https://api-mc.opsbunny.net/docs/public/swagger.json delete /registries/{registryId} Deletes a container registry. Returns an error if the registry is currently in use by any applications. # Find Container Image Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/find-container-image https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/images/find Parses an image reference like "ghcr.io/owner/app:v1" or "owner/app" and checks whether the image tag exists in the selected registries of the user (only those of the reference's host when it has one). Returns every registry that has the image tag with its id, hostname, the image namespace, name, tag and digest. # Get Container Config Suggestions Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/get-container-config-suggestions https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/config-suggestions Gets recommended configuration for a container image including endpoint configurations and environment variables. # Get Container Image Digest Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/get-container-image-digest https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/digest Retrieves the digest information for a specific container image tag. # Get Container Registry Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/get-container-registry https://api-mc.opsbunny.net/docs/public/swagger.json get /registries/{registryId} Retrieves a specific container registry by its ID. # Get Image Config Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/get-image-config https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/image-config Retrieves endpoint and volume suggestions for a container image from a registry. # List Container Image Tags Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/list-container-image-tags https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/tags Lists all available tags for a specific container image. # List Container Image Tags Page Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/list-container-image-tags-page https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/tags/paged Lists one page of the tags of a container image, in the order the registry returns them (newest first for Docker Hub images whose tags the registry can't list in time). The response cursor is the cursor of the next page; it is empty on the last page. # List Container Images Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/list-container-images https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/images Lists all container images available in a private registry. # List Container Registries Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/list-container-registries https://api-mc.opsbunny.net/docs/public/swagger.json get /registries Lists all container registries configured for the authenticated user. # Search Public Container Images Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/search-public-container-images https://api-mc.opsbunny.net/docs/public/swagger.json post /registries/public-images/search Searches for public container images in a registry by prefix. # Update Container Registry Source: https://bunny.net/docs/api-reference/magic-containers/containerregistries/update-container-registry https://api-mc.opsbunny.net/docs/public/swagger.json put /registries/{registryId} Updates an existing container registry configuration including credentials. # Add Container Template Source: https://bunny.net/docs/api-reference/magic-containers/containers/add-container-template https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/containers Adds a new container template to an application. # Delete Container Template Source: https://bunny.net/docs/api-reference/magic-containers/containers/delete-container-template https://api-mc.opsbunny.net/docs/public/swagger.json delete /apps/{appId}/containers/{containerId} Deletes a container template from an application. # Get Container Template Source: https://bunny.net/docs/api-reference/magic-containers/containers/get-container-template https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/containers/{containerId} Gets a container template within an application. # Patch Container Template Source: https://bunny.net/docs/api-reference/magic-containers/containers/patch-container-template https://api-mc.opsbunny.net/docs/public/swagger.json patch /apps/{appId}/containers/{containerId} Partially updates a container template within an application. Only provided fields will be updated; existing fields not included in the request will remain unchanged. # Set Container Environment Variables Source: https://bunny.net/docs/api-reference/magic-containers/containers/set-container-environment-variables https://api-mc.opsbunny.net/docs/public/swagger.json put /apps/{appId}/containers/{containerId}/env Replaces all environment variables for a container template. All existing environment variables will be removed and replaced with the provided set. # Add application endpoint Source: https://bunny.net/docs/api-reference/magic-containers/endpoints/add-application-endpoint https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/containers/{containerId}/endpoints Add CDN or Anycast endpoint to a container of given application. # Delete application endpoint Source: https://bunny.net/docs/api-reference/magic-containers/endpoints/delete-application-endpoint https://api-mc.opsbunny.net/docs/public/swagger.json delete /apps/{appId}/endpoints/{endpointId} Delete endpoint of a container for given application. # List application endpoints Source: https://bunny.net/docs/api-reference/magic-containers/endpoints/list-application-endpoints https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/endpoints List endpoints from all containers for given application # Update Application Endpoint Source: https://bunny.net/docs/api-reference/magic-containers/endpoints/update-application-endpoint https://api-mc.opsbunny.net/docs/public/swagger.json put /apps/{appId}/endpoints/{endpointId} Update an existing endpoint for given application # Get User Limits Source: https://bunny.net/docs/api-reference/magic-containers/limits/get-user-limits https://api-mc.opsbunny.net/docs/public/swagger.json get /limits Retrieves the current resource limits and usage for the authenticated user, including application counts and instance limits. # Create log forwarding configuration Source: https://bunny.net/docs/api-reference/magic-containers/log-forwarding/create-log-forwarding-configuration https://api-mc.opsbunny.net/docs/public/swagger.json post /log/forwarding Create a new log forwarding configuration. # Delete log-forwarding configuration Source: https://bunny.net/docs/api-reference/magic-containers/log-forwarding/delete-log-forwarding-configuration https://api-mc.opsbunny.net/docs/public/swagger.json delete /log/forwarding/{appId} Delete a log-forwarding configuration. # Get log-forwarding configuration Source: https://bunny.net/docs/api-reference/magic-containers/log-forwarding/get-log-forwarding-configuration https://api-mc.opsbunny.net/docs/public/swagger.json get /log/forwarding/{appId} Get a specific log-forwarding configuration by ID. # List log-forwarding configurations Source: https://bunny.net/docs/api-reference/magic-containers/log-forwarding/list-log-forwarding-configurations https://api-mc.opsbunny.net/docs/public/swagger.json get /log/forwarding Get a list of all log-forwarding configurations for the authenticated user. # Update log-forwarding configuration Source: https://bunny.net/docs/api-reference/magic-containers/log-forwarding/update-log-forwarding-configuration https://api-mc.opsbunny.net/docs/public/swagger.json put /log/forwarding/{appId} Update an existing log-forwarding configuration. # List Node IPs (Plain) Source: https://bunny.net/docs/api-reference/magic-containers/nodes/list-node-ips-plain https://api-mc.opsbunny.net/docs/public/swagger.json get /nodes/plain Lists all node IP addresses in the Magic Containers network as a flat list. # List Nodes Source: https://bunny.net/docs/api-reference/magic-containers/nodes/list-nodes https://api-mc.opsbunny.net/docs/public/swagger.json get /nodes Lists all node IP addresses in the Magic Containers network. # Magic Containers API Reference Source: https://bunny.net/docs/api-reference/magic-containers/overview Create, modify or delete your Magic Containers applications configuration. The Magic Containers API provides a RESTful interface for managing your magic containers applications. ## Base URL ``` https://api.bunny.net/mc ``` ## Authentication Authenticate using the `AccessKey` header with your Bunny Net Access Key: ```bash theme={null} curl --request GET \ --url https://api.bunny.net/mc/apps \ --header 'AccessKey: YOUR_API_KEY' ``` > [!NOTE] > > Find your API key in the [account API > Keys](https://dash.bunny.net/account/api-key). # Recreate Pod Source: https://bunny.net/docs/api-reference/magic-containers/pods/recreate-pod https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/pods/{podId}/recreate Recreate a pod, deleting previous one. # Get Optimal Base Region Source: https://bunny.net/docs/api-reference/magic-containers/regions/get-optimal-base-region https://api-mc.opsbunny.net/docs/public/swagger.json get /regions/optimal Returns the optimal base region for deployment based on the user's CDN server token location. # List Regions Source: https://bunny.net/docs/api-reference/magic-containers/regions/list-regions https://api-mc.opsbunny.net/docs/public/swagger.json get /regions Lists all available regions where applications can be deployed, including their anycast support and capacity status. # Get Application Region Settings Source: https://bunny.net/docs/api-reference/magic-containers/regionsettings/get-application-region-settings https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/region-settings Retrieves the current region settings for an application, including allowed regions, required regions, and maximum allowed regions. # Update Application Region Settings Source: https://bunny.net/docs/api-reference/magic-containers/regionsettings/update-application-region-settings https://api-mc.opsbunny.net/docs/public/swagger.json put /apps/{appId}/region-settings Updates the region settings for an application, including allowed regions, required regions, and maximum allowed regions. # Delete All Volume Instances Source: https://bunny.net/docs/api-reference/magic-containers/volumes/delete-all-volume-instances https://api-mc.opsbunny.net/docs/public/swagger.json delete /apps/{appId}/volumes/{volumeId} Deletes all volume instances for a volume template. All instances must be detached before deletion. # Delete Volume Instance Source: https://bunny.net/docs/api-reference/magic-containers/volumes/delete-volume-instance https://api-mc.opsbunny.net/docs/public/swagger.json delete /apps/{appId}/volumes/{volumeId}/instances/{instanceId} Deletes a specific volume instance. The volume must be detached before deletion. # Detach Volume Source: https://bunny.net/docs/api-reference/magic-containers/volumes/detach-volume https://api-mc.opsbunny.net/docs/public/swagger.json post /apps/{appId}/volumes/{volumeId}/detach Detaches a volume template from all application containers. # List Volumes Source: https://bunny.net/docs/api-reference/magic-containers/volumes/list-volumes https://api-mc.opsbunny.net/docs/public/swagger.json get /apps/{appId}/volumes Lists all volume templates and their instances for an application, including usage statistics. # Update Volume Source: https://bunny.net/docs/api-reference/magic-containers/volumes/update-volume https://api-mc.opsbunny.net/docs/public/swagger.json patch /apps/{appId}/volumes/{volumeId} Partially updates a volume template's configuration including name and size. Only provided fields will be updated. # Get origin error logs for a specific Pull Zone and date Source: https://bunny.net/docs/api-reference/origin-errors/get-origin-error-logs-for-a-specific-pull-zone-and-date /api-reference/origin-errors/openapi.json get /{pullZoneId}/{dateTime} Retrieves origin error logs for the given Pull Zone and date. # Origin Errors API Reference Source: https://bunny.net/docs/api-reference/origin-errors/index Retrieve origin error logs for your pull zones via HTTP. The Origin Errors API provides visibility into failed origin requests. Query error logs by pull zone and date to identify DNS failures, timeouts, and other origin issues. ## Base URL ``` https://cdn-origin-logging.bunny.net ``` ## Authentication Authenticate using the `AccessKey` header with your account API key: ```bash theme={null} curl --request GET \ --url https://cdn-origin-logging.bunny.net/{pullZoneId}/{dateTime} \ --header 'AccessKey: YOUR_API_KEY' ``` > [!NOTE] > > Find your API key in the [account > settings](https://dash.bunny.net/account/settings) under **API Keys**. ## Error codes The API returns logs with the following error codes: | Error Code | Description | | - | - | | `dns_lookup` | Origin DNS lookup failed | | `http_timeout` | Request to origin timed out | | `http_request_exception` | HTTP request exception occurred | | `http_request_failure` | HTTP request failed | | `http_invalid_range` | Invalid range request | | `http_loop_detected` | Request loop detected | | `http_invalid_compression` | Invalid compression in response | | `network_socket_exception` | Network socket exception | | `network_io_error` | Network I/O error | | `ssl_error` | TLS handshake with the origin failed (invalid origin certificate) | | `notfound_localdb` | Not found in local database | | `origin_url_malformed` | The configured origin URL is malformed | | `loadbalanced_origin_failure` | A load balancer origin failed | | `error` | Unknown or unclassified origin error | # Get Audit Log Source: https://bunny.net/docs/api-reference/partner/audit-log/get-audit-log https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json get /partner/customer/{customerUserId}/audit/{date} Get the audit log for one of the partner's customers. ResourceOwner identifies the customer each entry belongs to. # Download Invoice PDF Source: https://bunny.net/docs/api-reference/partner/billing/download-invoice-pdf https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json get /partner/customer/{customerUserId}/billing/{billingRecordId}/pdf Download the customer's invoice PDF for an existing billing record. # Get Customer Billing Source: https://bunny.net/docs/api-reference/partner/billing/get-customer-billing https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json get /partner/customer/{customerUserId}/billing Get a billing summary for the customer's account, including invoice history. # Get Contract Source: https://bunny.net/docs/api-reference/partner/contract/get-contract https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json get /partner/contract Get the contract details for the authenticated partner. # Create Customer Source: https://bunny.net/docs/api-reference/partner/customers/create-customer https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json post /partner/customers Create a new customer under the authenticated partner and email them a set-password welcome link. # Get Customer Overview Source: https://bunny.net/docs/api-reference/partner/customers/get-customer-overview https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json get /partner/customer/{customerUserId} Get an overview of a customer: monthly usage snapshot, bandwidth trend, contract, and product summaries. # Get Partner Overview Source: https://bunny.net/docs/api-reference/partner/customers/get-partner-overview https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json get /partner/overview Get an overview of the partner's own account in the same shape as a customer overview: monthly usage snapshot, bandwidth trend, and product resource counts. Contract fields are always empty and every product is enabled. # Invite Customer Source: https://bunny.net/docs/api-reference/partner/customers/invite-customer https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json post /partner/customers/invite Invite an existing bunny.net user to become a customer of the authenticated partner. An invitation email is sent, and the user is assigned once they accept while signed in. # List Customers Source: https://bunny.net/docs/api-reference/partner/customers/list-customers https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json get /partner/customers List the authenticated partner's assigned customers, newest assignment first. # Partner Portal API Reference Source: https://bunny.net/docs/api-reference/partner/index Manage your customers, pricing, products, and billing through the bunny.net Partner Portal API. The Partner Portal API provides a RESTful interface for bunny.net partners to manage their customers. Create and invite customers, review pricing, enable products, view billing, and access audit logs. > [!NOTE] > > This API is available to partners only. See the [Partner Portal overview](/docs/partners), or [contact us](https://bunny.net/contact/) if you would like to join the partner program. ## Base URL ``` https://api.bunny.net ``` ## Authentication Authenticate using the `AccessKey` header with your partner account API key: ```bash theme={null} curl --request GET \ --url https://api.bunny.net/partner/customers \ --header 'AccessKey: YOUR_API_KEY' ``` > [!NOTE] > > Find your API key in the [account > settings](https://dash.bunny.net/account/settings) under **API Keys**. ## Resources The Partner Portal API provides access to the following resources: | Resource | Description | | - | - | | Customers | Create, list, and invite customers under your partner account | | Pricing | Review your partner pricing, and the pricing applied to each customer | | Products | Enable or disable the products available to your customers | | Contract | Retrieve your partner contract details | | Billing | View customer billing records and download invoice PDFs | | Audit Log | Review the history of actions taken on a customer's account | # Get Customer Pricing Source: https://bunny.net/docs/api-reference/partner/pricing/get-customer-pricing https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json get /partner/customer/{customerUserId}/pricing Get the full pricing overview for a customer: effective prices versus list prices, custom overrides, and overrides expiring soon. # Get Partner Pricing Source: https://bunny.net/docs/api-reference/partner/pricing/get-partner-pricing https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json get /partner/pricing Get the full pricing overview for the authenticated partner: effective prices versus list prices, custom overrides, and overrides expiring soon. # List Customer Products Source: https://bunny.net/docs/api-reference/partner/products/list-customer-products https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json get /partner/customer/{customerUserId}/products List the customer's product enable/disable states, with resource counts per product. # Set Customer Product Source: https://bunny.net/docs/api-reference/partner/products/set-customer-product https://core-api-public-docs.b-cdn.net/docs/v3/PartnerPortal.json put /partner/customer/{customerUserId}/products/{productId} Enable or disable a product on this partner-customer relationship. # Quickstart Source: https://bunny.net/docs/api-reference/quickstart Make your first API request to bunny.net in minutes. This guide walks you through making your first API request to bunny.net. **1. Get your API key** 1. Log in to the [bunny.net dashboard](https://dash.bunny.net) 2. Navigate to **Account** > **API** 3. Copy your **Account API Key** > [!WARNING] > > Keep your API key secure and never share it publicly. **2. Make your first request** Use the following curl command to list your pull zones: ```bash theme={null} curl --request GET \ --url https://api.bunny.net/pullzone \ --header 'AccessKey: YOUR_API_KEY' \ --header 'Content-Type: application/json' ``` Replace `YOUR_API_KEY` with the API key you copied from the dashboard. **3. View the response** A successful response returns a list of your pull zones: ```json theme={null} { "Items": [ { "Id": 1234, "Name": "my-pull-zone", "OriginUrl": "https://example.com", "Enabled": true, "Hostnames": [ { "Id": 5678, "Value": "my-pull-zone.b-cdn.net" } ] } ], "CurrentPage": 0, "TotalItems": 1, "HasMoreItems": false } ``` If you don't have any pull zones yet, the `Items` array will be empty. **4. Integrate with other APIs** Explore the full range of bunny.net APIs: > **[Core Platform](/docs/api-reference/core)** > **[Edge Storage](/docs/api-reference/storage)** > **[Stream](/docs/api-reference/stream)** > **[Shield](/docs/api-reference/shield)** > **[Scripting](/docs/api-reference/scripting)** > **[Origin Errors](/docs/api-reference/origin-errors)** # Get Code Source: https://bunny.net/docs/api-reference/scripting/code/get-code https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id}/code # Set Code Source: https://bunny.net/docs/api-reference/scripting/code/set-code https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/code # Add Edge Script Source: https://bunny.net/docs/api-reference/scripting/edge-script/add-edge-script https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script # Delete Edge Script Source: https://bunny.net/docs/api-reference/scripting/edge-script/delete-edge-script https://core-api-public-docs.b-cdn.net/docs/v3/compute.json delete /compute/script/{id} # Get Edge Script Source: https://bunny.net/docs/api-reference/scripting/edge-script/get-edge-script https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id} # Get Edge Script Statistics Source: https://bunny.net/docs/api-reference/scripting/edge-script/get-edge-script-statistics https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id}/statistics # List Edge Scripts Source: https://bunny.net/docs/api-reference/scripting/edge-script/list-edge-scripts https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script # Rotate Deployment Key Source: https://bunny.net/docs/api-reference/scripting/edge-script/rotate-deployment-key https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/deploymentKey/rotate # Update Edge Script Source: https://bunny.net/docs/api-reference/scripting/edge-script/update-edge-script https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id} # Edge Scripting API Reference Source: https://bunny.net/docs/api-reference/scripting/index Deploy, manage, and monitor edge scripts via HTTP. The Scripting API lets you manage edge scripts programmatically. Create scripts, configure deployments, and monitor execution via HTTP. ## Base URL ``` https://api.bunny.net ``` ## Authentication Authenticate using the `AccessKey` header with your account API key: ```bash theme={null} curl --request GET \ --url https://api.bunny.net/compute/script \ --header 'AccessKey: YOUR_API_KEY' ``` > [!NOTE] > > Find your API key in the [account > settings](https://dash.bunny.net/account/settings) under **API Keys**. ## Script Types Edge Scripting supports two script types: * **Standalone** — Handle requests directly without an origin server * **Middleware** — Intercept and modify requests/responses flowing through a pull zone See the [Edge Scripting documentation](/docs/scripting) for details on each type. ## SDKs > **[TypeScript](https://github.com/nicosommi/bunnycdn-api-client)** # Get Active Release Source: https://bunny.net/docs/api-reference/scripting/release/get-active-release https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id}/releases/active # Get Releases Source: https://bunny.net/docs/api-reference/scripting/release/get-releases https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id}/releases # Publish Release Source: https://bunny.net/docs/api-reference/scripting/release/publish-release https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/publish # Publish Release Source: https://bunny.net/docs/api-reference/scripting/release/publish-release-1 https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/publish/{uuid} # Add Secret Source: https://bunny.net/docs/api-reference/scripting/secret/add-secret https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/secrets # Delete Secret Source: https://bunny.net/docs/api-reference/scripting/secret/delete-secret https://core-api-public-docs.b-cdn.net/docs/v3/compute.json delete /compute/script/{id}/secrets/{secretId} # List Secrets Source: https://bunny.net/docs/api-reference/scripting/secret/list-secrets https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id}/secrets # Update Secret Source: https://bunny.net/docs/api-reference/scripting/secret/update-secret https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/secrets/{secretId} # Upsert Secret Source: https://bunny.net/docs/api-reference/scripting/secret/upsert-secret https://core-api-public-docs.b-cdn.net/docs/v3/compute.json put /compute/script/{id}/secrets # Add Variable Source: https://bunny.net/docs/api-reference/scripting/variable/add-variable https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/variables/add # Delete Variable Source: https://bunny.net/docs/api-reference/scripting/variable/delete-variable https://core-api-public-docs.b-cdn.net/docs/v3/compute.json delete /compute/script/{id}/variables/{variableId} # Get Variable Source: https://bunny.net/docs/api-reference/scripting/variable/get-variable https://core-api-public-docs.b-cdn.net/docs/v3/compute.json get /compute/script/{id}/variables/{variableId} # Update Variable Source: https://bunny.net/docs/api-reference/scripting/variable/update-variable https://core-api-public-docs.b-cdn.net/docs/v3/compute.json post /compute/script/{id}/variables/{variableId} # Upsert Variable Source: https://bunny.net/docs/api-reference/scripting/variable/upsert-variable https://core-api-public-docs.b-cdn.net/docs/v3/compute.json put /compute/script/{id}/variables # Create a new Custom Access List associated with a Shield Zone Source: https://bunny.net/docs/api-reference/shield/access-lists/create-a-new-custom-access-list-associated-with-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json post /shield/shield-zone/{shieldZoneId}/access-lists # Delete the specified Custom Access List associated with a Shield Zone Source: https://bunny.net/docs/api-reference/shield/access-lists/delete-the-specified-custom-access-list-associated-with-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json delete /shield/shield-zone/{shieldZoneId}/access-lists/{id} # Get all Access Lists API enumeration types and their values Source: https://bunny.net/docs/api-reference/shield/access-lists/get-all-access-lists-api-enumeration-types-and-their-values https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/access-lists/enums # Get all Access Lists available for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/access-lists/get-all-access-lists-available-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/access-lists # Get the specified Custom Access List associated with a Shield Zone Source: https://bunny.net/docs/api-reference/shield/access-lists/get-the-specified-custom-access-list-associated-with-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/access-lists/{id} # Update Access List Configuration for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/access-lists/update-access-list-configuration-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/access-lists/configurations/{id} # Update the specified Custom Access List associated with a Shield Zone Source: https://bunny.net/docs/api-reference/shield/access-lists/update-the-specified-custom-access-list-associated-with-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/access-lists/{id} # Get all API Guardian enumeration types and their values Source: https://bunny.net/docs/api-reference/shield/api-guardian/get-all-api-guardian-enumeration-types-and-their-values https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/api-guardian/enums # Get the API Guardian configuration and endpoints. Source: https://bunny.net/docs/api-reference/shield/api-guardian/get-the-api-guardian-configuration-and-endpoints https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/api-guardian # Update the API Guardian configuration (enabled, execution mode, body limit action) Source: https://bunny.net/docs/api-reference/shield/api-guardian/update-the-api-guardian-configuration-enabled-execution-mode-body-limit-action https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/api-guardian # Update your API Guardian Endpoint configuration Source: https://bunny.net/docs/api-reference/shield/api-guardian/update-your-api-guardian-endpoint-configuration https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/api-guardian/endpoint/{endpointId} # Update your OpenAPI specification Source: https://bunny.net/docs/api-reference/shield/api-guardian/update-your-openapi-specification https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/api-guardian/spec # Upload your OpenAPI specification Source: https://bunny.net/docs/api-reference/shield/api-guardian/upload-your-openapi-specification https://api.bunny.net/shield/docs/v1/swagger.json post /shield/shield-zone/{shieldZoneId}/api-guardian/spec # List bots available for explicit allow/block configuration on this Shield Zone, grouped by category. Source: https://bunny.net/docs/api-reference/shield/bot-categorization/list-bots-available-for-explicit-allowblock-configuration-on-this-shield-zone-grouped-by-category https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/bot-categorization # Set or clear the action applied to a categorised bot for this Shield Zone. Source: https://bunny.net/docs/api-reference/shield/bot-categorization/set-or-clear-the-action-applied-to-a-categorised-bot-for-this-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json put /shield/shield-zone/{shieldZoneId}/bot-categorization/bots/{botId} # Set or clear the action applied to every bot in a category for this Shield Zone. Source: https://bunny.net/docs/api-reference/shield/bot-categorization/set-or-clear-the-action-applied-to-every-bot-in-a-category-for-this-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json put /shield/shield-zone/{shieldZoneId}/bot-categorization/categories/{category} # Update your current Bot Detection configuration Source: https://bunny.net/docs/api-reference/shield/bot-detection/update-your-current-bot-detection-configuration https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/bot-detection # Your current Bot Detection configuration Source: https://bunny.net/docs/api-reference/shield/bot-detection/your-current-bot-detection-configuration https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/bot-detection # Delete a custom HTML response page for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/custom-response-pages/delete-a-custom-html-response-page-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json delete /shield/shield-zone/{shieldZoneId}/custom-page/{pageType} # Get a custom HTML response page for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/custom-response-pages/get-a-custom-html-response-page-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/custom-page/{pageType} # Upload a custom HTML response page for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/custom-response-pages/upload-a-custom-html-response-page-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json put /shield/shield-zone/{shieldZoneId}/custom-page/{pageType} # List of all DDoS Enum Mappings Source: https://bunny.net/docs/api-reference/shield/ddos/list-of-all-ddos-enum-mappings https://api.bunny.net/shield/docs/v1/swagger.json get /shield/ddos/enums # Export the full filtered Event Logs set for a Shield Zone as CSV Source: https://bunny.net/docs/api-reference/shield/event-logs/export-the-full-filtered-event-logs-set-for-a-shield-zone-as-csv https://api.bunny.net/shield/docs/v1/swagger.json post /shield/event-logs/{shieldZoneId}/export # Get Event Logs for Shield Zone Source: https://bunny.net/docs/api-reference/shield/event-logs/get-event-logs-for-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/event-logs/{shieldZoneId}/{date}/{continuationToken} # Search, filter and group Event Logs for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/event-logs/search-filter-and-group-event-logs-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json post /shield/event-logs/{shieldZoneId}/search # Shield API Reference Source: https://bunny.net/docs/api-reference/shield/index Configure WAF rules, rate limiting, and security settings via HTTP. The Shield API provides a RESTful interface for managing security settings on your pull zones. Configure WAF rules, rate limiting, bot detection, and access controls programmatically. ## Base URL ``` https://api.bunny.net ``` ## Authentication Authenticate using the `AccessKey` header with your account API key: ```bash theme={null} curl --request GET \ --url https://api.bunny.net/shield/waf/{shieldZoneId} \ --header 'AccessKey: YOUR_API_KEY' ``` > [!NOTE] > > Find your API key in the [account > settings](https://dash.bunny.net/account/settings) under **API Keys**. ## Resources > **[WAF](/docs/shield/waf)** > > Block exploits and OWASP Top 10 vulnerabilities > **[Rate Limiting](/docs/shield/rate-limiting)** > > Control request rates per IP, user, or path > **[Bot Detection](/docs/shield/bot-detection)** > > Detect and block malicious bots > **[Access Lists](/docs/shield/access-lists)** > > Manage IP allowlists and blocklists # Get a detailed metrics overview for the specified Shield Zone within the selected time range and resolution Source: https://bunny.net/docs/api-reference/shield/metrics/get-a-detailed-metrics-overview-for-the-specified-shield-zone-within-the-selected-time-range-and-resolution https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/overview/{shieldZoneId}/detailed # Get aggregated rate limit metrics for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-aggregated-rate-limit-metrics-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/rate-limits/{shieldZoneId} # Get an overview of metrics for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-an-overview-of-metrics-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/overview/{shieldZoneId} # Get API Guardian metrics for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-api-guardian-metrics-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/shield-zone/{shieldZoneId}/api-guardian # Get bot detection metrics for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-bot-detection-metrics-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/shield-zone/{shieldZoneId}/bot-detection # Get detailed metrics for the specified Rate Limit Source: https://bunny.net/docs/api-reference/shield/metrics/get-detailed-metrics-for-the-specified-rate-limit https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/rate-limit/{id} # Get metrics for a specific API Guardian endpoint within the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-metrics-for-a-specific-api-guardian-endpoint-within-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/shield-zone/{shieldZoneId}/api-guardian/endpoint/{endpointId} # Get metrics for a specific WAF Rule within the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-metrics-for-a-specific-waf-rule-within-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/shield-zone/{shieldZoneId}/waf-rule/{ruleId} # Get the overage breakdown for the specified Shield Zone for a given month, segmented by billing plan changes Source: https://bunny.net/docs/api-reference/shield/metrics/get-the-overage-breakdown-for-the-specified-shield-zone-for-a-given-month-segmented-by-billing-plan-changes https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/overages/{shieldZoneId} # Get upload scanning metrics for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/metrics/get-upload-scanning-metrics-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/metrics/shield-zone/{shieldZoneId}/upload-scanning # Get the current Promotion State for your Account Source: https://bunny.net/docs/api-reference/shield/promotions/get-the-current-promotion-state-for-your-account https://api.bunny.net/shield/docs/v1/swagger.json get /shield/promo/state # Create a Rate Limit for your Shield Zone Source: https://bunny.net/docs/api-reference/shield/rate-limiting/create-a-rate-limit-for-your-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json post /shield/rate-limit # Delete a Rate Limit on your Shield Zone Source: https://bunny.net/docs/api-reference/shield/rate-limiting/delete-a-rate-limit-on-your-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json delete /shield/rate-limit/{id} # Get Individual Rate Limit for your Shield Zone Source: https://bunny.net/docs/api-reference/shield/rate-limiting/get-individual-rate-limit-for-your-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/rate-limit/{id} # Get Rate Limits for your Shield Zone Source: https://bunny.net/docs/api-reference/shield/rate-limiting/get-rate-limits-for-your-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/rate-limits/{shieldZoneId} # Update a Rate Limit configuration on your Shield Zone Source: https://bunny.net/docs/api-reference/shield/rate-limiting/update-a-rate-limit-configuration-on-your-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/rate-limit/{id} # Create a Shield Zone for your PullZone Source: https://bunny.net/docs/api-reference/shield/shield-zone/create-a-shield-zone-for-your-pullzone https://api.bunny.net/shield/docs/v1/swagger.json post /shield/shield-zone # Create a Shield Zone in under-attack mode for your PullZone Source: https://bunny.net/docs/api-reference/shield/shield-zone/create-a-shield-zone-in-under-attack-mode-for-your-pullzone https://api.bunny.net/shield/docs/v1/swagger.json post /shield/shield-zone/under-attack # Get Active Shield Zones for Pullzone Mapping Source: https://bunny.net/docs/api-reference/shield/shield-zone/get-active-shield-zones-for-pullzone-mapping https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zones/pullzone-mapping # Get all of your Shield Zone Configurations Source: https://bunny.net/docs/api-reference/shield/shield-zone/get-all-of-your-shield-zone-configurations https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zones # Get Singular Shield Zone Configuration Source: https://bunny.net/docs/api-reference/shield/shield-zone/get-singular-shield-zone-configuration https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId} # Get Singular Shield Zone Configuration for PullZone Source: https://bunny.net/docs/api-reference/shield/shield-zone/get-singular-shield-zone-configuration-for-pullzone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/get-by-pullzone/{pullZoneId} # Get the recommended defaults for creating a Shield Zone Source: https://bunny.net/docs/api-reference/shield/shield-zone/get-the-recommended-defaults-for-creating-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/defaults # Update your Shield Zone configuration Source: https://bunny.net/docs/api-reference/shield/shield-zone/update-your-shield-zone-configuration https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone # Get your Current Upload Scanning Configuration Source: https://bunny.net/docs/api-reference/shield/upload-scanning/get-your-current-upload-scanning-configuration https://api.bunny.net/shield/docs/v1/swagger.json get /shield/shield-zone/{shieldZoneId}/upload-scanning # Update your Upload Scanning Configuration Source: https://bunny.net/docs/api-reference/shield/upload-scanning/update-your-upload-scanning-configuration https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/shield-zone/{shieldZoneId}/upload-scanning # Create a new custom WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/create-a-new-custom-waf-rule https://api.bunny.net/shield/docs/v1/swagger.json post /shield/waf/custom-rule # Delete a custom WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/delete-a-custom-waf-rule https://api.bunny.net/shield/docs/v1/swagger.json delete /shield/waf/custom-rule/{id} # Retrieve a specific custom WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-a-specific-custom-waf-rule https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/custom-rule/{id} # Retrieve all available WAF enum mappings Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-all-available-waf-enum-mappings https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/enums # Retrieve all available WAF profiles Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-all-available-waf-profiles https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/profiles # Retrieve all available WAF rules for a Shield Zone Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-all-available-waf-rules-for-a-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/rules/{shieldZoneId} # Retrieve an AI recommendation for a triggered WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-an-ai-recommendation-for-a-triggered-waf-rule https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/rules/review-triggered/ai-recommendation/{shieldZoneId}/{ruleId} # Retrieve custom WAF rules configured for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-custom-waf-rules-configured-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/custom-rules/{shieldZoneId} # Retrieve the default WAF engine configuration Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-the-default-waf-engine-configuration https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/engine-config # Retrieve WAF rules segmented by subscription plan Source: https://bunny.net/docs/api-reference/shield/waf/retrieve-waf-rules-segmented-by-subscription-plan https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/rules/plan-segmentation # Review all triggered WAF rules for the specified Shield Zone Source: https://bunny.net/docs/api-reference/shield/waf/review-all-triggered-waf-rules-for-the-specified-shield-zone https://api.bunny.net/shield/docs/v1/swagger.json get /shield/waf/rules/review-triggered/{shieldZoneId} # Review and update the action of a triggered WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/review-and-update-the-action-of-a-triggered-waf-rule https://api.bunny.net/shield/docs/v1/swagger.json post /shield/waf/rules/review-triggered/{shieldZoneId} # Update an existing custom WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/update-an-existing-custom-waf-rule https://api.bunny.net/shield/docs/v1/swagger.json put /shield/waf/custom-rule/{id} # Update an existing custom WAF rule Source: https://bunny.net/docs/api-reference/shield/waf/update-an-existing-custom-waf-rule-1 https://api.bunny.net/shield/docs/v1/swagger.json patch /shield/waf/custom-rule/{id} # List Files Source: https://bunny.net/docs/api-reference/storage/browse-files/list-files /api-reference/storage/openapi.json get /{storageZoneName}/{path}/ Retrieve a list of files and directories located in the given directory. # Storage API Reference Source: https://bunny.net/docs/api-reference/storage/index Upload, download, and manage files in your storage zones via HTTP. The Edge Storage API provides a simple RESTful interface for managing files in your storage zones. ## Base URL ``` https://{region}.storage.bunnycdn.com ``` The endpoint depends on your storage zone's primary region. See [storage endpoints](/docs/storage/http#storage-endpoints) for all available regions. ## Authentication Authenticate using the `AccessKey` header with your storage zone password: ```bash theme={null} curl --request GET \ --url https://storage.bunnycdn.com/{storageZoneName}/ \ --header 'AccessKey: YOUR_STORAGE_ZONE_PASSWORD' ``` > [!NOTE] > > Use your storage zone password, not your account API key. Find it in the **FTP > & API Access** tab of your storage zone. ## SDKs > **[TypeScript](/docs/storage/typescript-sdk)** > **[PHP](/docs/storage/php-sdk)** > **[.NET](/docs/storage/dotnet-sdk)** > **[Java](/docs/storage/java-sdk)** # Delete File Source: https://bunny.net/docs/api-reference/storage/manage-files/delete-file /api-reference/storage/openapi.json delete /{storageZoneName}/{path}/{fileName} Delete an object from the storage zone. In case the object is a directory all the data in it will be recursively deleted as well. # Download File Source: https://bunny.net/docs/api-reference/storage/manage-files/download-file /api-reference/storage/openapi.json get /{storageZoneName}/{path}/{fileName} Returns the stored file at the given path. If the file does not exist, a 404 response will be returned. # Upload File Source: https://bunny.net/docs/api-reference/storage/manage-files/upload-file /api-reference/storage/openapi.json put /{storageZoneName}/{path}/{fileName} Upload a file to a storage zone based on the URL path. If the directory tree does not exist, it will be created automatically. **The file content should be sent as the body of the request without any type of encoding.** # Get Encoding Statistics Source: https://bunny.net/docs/api-reference/stream/encoding-statistics/get-encoding-statistics https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/statistics/encoding Returns video encoding seconds for the library, split into premium and free tier. Each includes billable (normalized) and output seconds over time, plus totals by video codec, resolution, and JIT vs standard encoding. Data is available starting 2026-06-15. # Stream API Reference Source: https://bunny.net/docs/api-reference/stream/index Upload, manage, and deliver videos with the Stream API. The Stream API provides a RESTful interface for managing video libraries, uploading videos, and controlling playback settings. ## Base URL ``` https://video.bunnycdn.com ``` ## Authentication Authenticate using the `AccessKey` header with your Stream API key: ```bash theme={null} curl --request GET \ --url https://video.bunnycdn.com/library/{libraryId}/videos \ --header 'AccessKey: YOUR_STREAM_API_KEY' ``` > [!NOTE] > > Find your Stream API key in the **API** section of your video library > settings. See [Authentication](/docs/stream/authentication) for details. ## Resources > **[Upload Videos](/docs/stream/http-api)** > **[Embed Videos](/docs/stream/embedding)** > **[Collections](/docs/stream/collections)** > **[Webhooks](/docs/stream/webhooks)** # Create Collection Source: https://bunny.net/docs/api-reference/stream/manage-collections/create-collection https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/collections # Delete Collection Source: https://bunny.net/docs/api-reference/stream/manage-collections/delete-collection https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json delete /library/{libraryId}/collections/{collectionId} Permanently deletes the collection together with every video and live stream it contains — nothing is moved out of the collection first, it is deleted along with it. Fails with 400 if the collection contains a currently running live stream; stop it before deleting the collection. This cannot be undone. # Get Collection Source: https://bunny.net/docs/api-reference/stream/manage-collections/get-collection https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/collections/{collectionId} Returns the details of a collection. Collections can group both videos and live streams, reflected separately in VideoCount and LiveStreamCount; PreviewImageUrls only reflects the videos in the collection. # Get Collection List Source: https://bunny.net/docs/api-reference/stream/manage-collections/get-collection-list https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/collections Returns a paginated list of collections for the library. Collections can group both videos and live streams, reflected separately in VideoCount and LiveStreamCount; preview thumbnails only reflect the videos in each collection. When includeThumbnails is true, preview thumbnails are only populated for the first 60 collections of the returned page — later items in a larger page are returned without thumbnails. # Update Collection Source: https://bunny.net/docs/api-reference/stream/manage-collections/update-collection https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/collections/{collectionId} # Add Caption Source: https://bunny.net/docs/api-reference/stream/manage-videos/add-caption https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/captions/{srclang} # Add output codec to video Source: https://bunny.net/docs/api-reference/stream/manage-videos/add-output-codec-to-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json put /library/{libraryId}/videos/{videoId}/outputs/{outputCodecId} Encodes and adds an additional output codec to an already-encoded video. Requires Premium Encoding to be enabled for the library and the codec to already be enabled for the library, and fails if the original source file is missing or the codec was already processed. # Cleanup unconfigured resolutions Source: https://bunny.net/docs/api-reference/stream/manage-videos/cleanup-unconfigured-resolutions https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/resolutions/cleanup Deletes stored resolution files that are no longer configured for the library, or the specific resolutions passed in resolutionsToDelete/allResolutions. This cannot be undone. If the library has 'keep original files' turned off, the original source file is also deleted as part of this cleanup even when deleteOriginal is not set. # Create Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/create-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos # Delete Caption Source: https://bunny.net/docs/api-reference/stream/manage-videos/delete-caption https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json delete /library/{libraryId}/videos/{videoId}/captions/{srclang} # Delete Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/delete-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json delete /library/{libraryId}/videos/{videoId} Deletes the video. This cannot be undone. The video stops appearing in lookups right away, but playback may still be possible for some time afterward before access is fully revoked. # Fetch Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/fetch-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/fetch # Get Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/get-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos/{videoId} # Get Video Heatmap Source: https://bunny.net/docs/api-reference/stream/manage-videos/get-video-heatmap https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos/{videoId}/heatmap Returns the attention heatmap for a specific video, showing relative viewer interest across the timeline. May be unavailable if the feature is disabled or there isn't enough viewing data. # Get Video heatmap data Source: https://bunny.net/docs/api-reference/stream/manage-videos/get-video-heatmap-data https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos/{videoId}/play/heatmap Returns raw heatmap data for the video, used to render the attention heatmap on the player timeline. Returns an empty string if the library has heatmaps disabled or no heatmap data exists yet. # Get Video play data Source: https://bunny.net/docs/api-reference/stream/manage-videos/get-video-play-data https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos/{videoId}/play Returns playback URLs and metadata for the video. For libraries with token authentication enabled, token and expires must be a valid signed pair or the request is rejected. # Get Video Statistics Source: https://bunny.net/docs/api-reference/stream/manage-videos/get-video-statistics https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/statistics Returns time-series views and watch time, plus country-level aggregates, at the library level or for a specific video. Control the time window with dateFrom/dateTo and the granularity with hourly. Statistics are available for the last 1 year. Basic safeguards prevent spam and bot inflation by de-duplicating sessions and ignoring obviously invalid events. # Get video storage size info Source: https://bunny.net/docs/api-reference/stream/manage-videos/get-video-storage-size-info https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos/{videoId}/storage # List Videos Source: https://bunny.net/docs/api-reference/stream/manage-videos/list-videos https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos # Reencode Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/reencode-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/reencode Re-encodes the video from its stored original file. Requires the original file to still be present in storage. If the library has transcribing enabled, this also re-queues (and re-bills) transcription. # Repackage Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/repackage-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/repackage Repackages the video from its existing .ts/.m4s segments without re-transcoding, primarily used to apply Enterprise DRM. # Set Thumbnail Source: https://bunny.net/docs/api-reference/stream/manage-videos/set-thumbnail https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/thumbnail Sets the video's thumbnail. If thumbnailUrl is omitted, the thumbnail is read from the request body as a raw image upload. If thumbnailUrl is one of thumbnail_1.jpg…thumbnail_5.jpg, an existing auto-generated thumbnail for the video is used instead. Any other value is fetched by the server as a URL, so it must point to a reachable, publicly accessible image. # Transcribe video Source: https://bunny.net/docs/api-reference/stream/manage-videos/transcribe-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/transcribe Queues transcription for the video, generating captions. By default this is blocked if the video was already auto-transcribed; set force to true to re-queue (and re-bill) transcription anyway. # Trigger Smart actions Source: https://bunny.net/docs/api-reference/stream/manage-videos/trigger-smart-actions https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId}/smart Queues Smart Actions (title, description, chapters and/or moments generation) for a video that already has captions. This endpoint does not check the video library's Smart Actions setting — requests are accepted, processed and billed for transcription usage even when Smart Actions is disabled for the library. Requires at least one of GenerateTitle, GenerateDescription, GenerateChapters or GenerateMoments, and returns 429 once the library's Smart Actions usage threshold for the video is exceeded. # Update Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/update-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json post /library/{libraryId}/videos/{videoId} # Upload Video Source: https://bunny.net/docs/api-reference/stream/manage-videos/upload-video https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json put /library/{libraryId}/videos/{videoId} # Video resolutions info Source: https://bunny.net/docs/api-reference/stream/manage-videos/video-resolutions-info https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /library/{libraryId}/videos/{videoId}/resolutions # Get oEmbed data Source: https://bunny.net/docs/api-reference/stream/oembed/get-oembed-data https://video.bunnycdn.com/openapi/bunnynet-video-api.public.json get /OEmbed Implements the oEmbed specification (oembed.com) for bunny.net video embed URLs, allowing third-party sites and tools to auto-generate an embeddable player for a pasted video link. url must be a bunny.net player URL for an existing video — either the legacy player (e.g. "https://iframe.mediadelivery.net/embed/{libraryId}/{videoId}") or the new player (e.g. "https://player.mediadelivery.net/embed/{libraryId}/{videoId}") — not an arbitrary link. Only the library ID and video ID segments are read, so /play/ URLs are accepted as well as /embed/ ones. For private libraries, the request must satisfy the library's referer restrictions and, separately, a valid token and expires pair if token authentication is enabled — one does not substitute for the other. # Add Funds Source: https://bunny.net/docs/billing/add-funds Add credit to your bunny.net account balance. bunny.net operates on a Pay As You Go model. You add credit to your account, and charges are deducted as you use services. You only pay for what you actually use. You can add funds from the [Billing](https://dash.bunny.net/account/billing) page by clicking **Recharge Account**. ![Add funds](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/recharge-account.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=6f06ba5eb2b32eef709f72acc2c7fa8e) ## Adding funds **1. Go to Billing** Navigate to [Billing](https://dash.bunny.net/account/billing) in your dashboard. **2. Click Recharge Account** Click the **Recharge Account** button. **3. Choose the amount** Select an amount: \$10, \$25, \$50, \$100, \$250, \$500, \$1000, \$2000, or enter a custom amount. **4. Select a payment method** Choose **Card or PayPal** or **Crypto**, then select or add a payment method. **5. Complete payment** Click **Pay** to add the funds to your account. > [!NOTE] > > You get a receipt for each payment, not an invoice. Invoices are issued > monthly for your usage. See [Billing History & > Invoices](/docs/billing/billing-history#receipts-and-invoices). ## Accepted payment methods We accept the following payment methods with no extra fees: * PayPal * Visa * Mastercard * American Express * Discover * JCB * Diner's Club * Bitcoin * Apple Pay > [!NOTE] > > Your credit card information is never sent to bunny.net. Payment details are > securely stored by Braintree. ## Applying a promo code If you have a promo code, click **Enter Promo Code** on the Billing page, enter your code, and click **Apply Code**. ![Enter Promo Code](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/enter-promo-code.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=eca4b8099dca41e3a2e4ffef5bd0291a) ## How billing works ### Traffic billing You are charged for each outbound byte transferred from the bunny.net network. Incoming traffic from your server to our network is free. If a 1GB video is viewed 100 times, you are charged for 100GB. Partial downloads are only charged for the bytes transferred. ### Storage billing Storage is charged based on usage in the past period, usually less than 24 hours. Data that is uploaded and deleted within this period is only charged for that time, not the whole month. ### Minimum monthly usage Your credits never expire. Note that trial credits are an exception: they are removed at the end of your trial period and any unused balance is not carried over. However, there is a \$1 minimum monthly usage while you have active zones on your account. If your usage is below \$1, it will be rounded up to \$1 for that month. This applies to the entire account, not per zone. This includes [Bunny DNS](/docs/dns). DNS hosting has no charges of its own, but a DNS zone that answers queries is active usage on your account. If DNS is the only service you use, you are still charged the \$1 minimum for that month. ### Minimum account balance You need to maintain a positive balance to keep your services online. If your balance drops low, you'll receive warning emails. If your balance becomes negative and is not topped up, your account is disabled and, after 60 days, your stored data is deleted. See [Negative Balance](/docs/billing/negative-balance). > [!NOTE] > > To avoid interruptions, consider enabling > [Auto-Recharge](/docs/billing/auto-recharge) to automatically add funds when your > balance gets low. # Best Practices Manual Source: https://bunny.net/docs/billing/affiliate-best-practices A step-by-step guide to getting started with the Bunny Affiliate Program. # Getting started with the Bunny Affiliate Program: Best practices You love bunny.net and want to share it with the world? Awesome! The Bunny Affiliate Program is here to help you do just that *and* earn some sweet rewards while you're at it. Whether you're new to affiliate marketing or a seasoned pro, here's a simple, step-by-step guide to get you hopping in the right direction. ## Step 1: Know your audience Before you start spreading the hop, take a breath and ask yourself who you're talking to. The better you understand your audience, the easier it is to show how [bunny.net](https://bunny.net/) can help them hop ahead. And the more carrots you'll collect along the way. Here are a few common examples: * **Game devs** facing dragging updates? Suggest Bunny CDN that delivers patches and updates at lightning speed. * **Bloggers or small businesses** frustrated with slow-loading sites? Show them how Bunny CDN speeds things up globally. * **Developers** building web apps or media-heavy sites? Recommend Bunny Storage + CDN for simple, blazing-fast delivery. **Tip**: Shape your message to fit the moment. A quick tweet for devs isn't the same as a YouTube deep dive for content creators. Meet people where they are, and speak to what they care about. ### Example scenarios (Steal these!) Here's how you can turn everyday conversations into affiliate wins: * **Online course creators / EdTech platforms:** "My course videos used to buffer like crazy for some students. Since switching to [bunny.net](https://bunny.net/), everything streams smoothly even halfway across the world." * **Tech blog post:** "I recently switched my blog to Bunny CDN and load times dropped by xy%. Here's how you can do it too (affiliate link)." * **Developer forum reply:** "If you're looking for a fast and reliable way to serve static files or video, I've had great results using bunny.net." *** ### Quick tips for maximum impact * **Be specific**: Don't just say it's great. Share what changed and how that impact showed up. * **Speak in your own voice**: Authentic beats polished. Your real experience goes further than any sales pitch. * **Show the results**: Speed tests, before-and-afters, real numbers. That's what builds trust and gets people's attention. *** ### Ready to hop in? Start small! One audience. One use case. One post. Share your story, add your link, and watch what happens. You might be surprised how fast the Bunny word gets around. *** ## Step 2: Pick your channels Now that you know *who* you're talking to, let's talk about *where*. There are tons of great places to share your bunny.net affiliate link. Just pick the ones that fit your style. ### 1. Blog about Bunny Got a blog? Sweet! That's the perfect place to share how bunny.net helped you and how it can help your readers hop ahead too. * **How:** Write a post like "5 Ways I Made My Website Blazing Fast" or a step-by-step tutorial. * **Pro tip:** Add your affiliate link where it fits naturally. No need to hard-sell. Just focus on telling your story. * **Example:** *"I shaved 5 seconds off my site's load time with bunny.net. Visitors don't even have time to blink! Want to try it too? Here's what I used: \[your link]."* ### 2. Create a YouTube video or vlog Prefer talking to typing? Video is gold, especially when you can *show* how bunny.net performs. * **How:** Record a tutorial, review, or even a casual "how I fixed my slow site" story. * **Pro tip:** Say your affiliate link out loud during the video *and* include it in the description. * **Example:** *"My site used to crawl. Seriously. But then I found bunny.net. It's been a total game changer. Want to see what I mean? Link's below!"* ### 3. Share on social media Whether you're into Twitter (X), Instagram, TikTok, or LinkedIn, your followers might love a speed boost too. * **How:** Post your experience, tips, or even funny "before and after" stories. * **Pro tip:** Use visuals. Think memes, GIFs, or screenshots. You can grab ready-to-go creatives from our affiliate assets library! * **Example post:** *"Why does my site load in the blink of an eye? Meet @BunnyCDN. They're behind the magic. Here's the link I used: \[your link]"* Don't forget to tag us! We'd love to reshare your content. * [Facebook](https://www.facebook.com/bunnycdn/) * [Twitter (X)](https://twitter.com/BunnyCDN) * [LinkedIn](https://www.linkedin.com/company/bunnynet) * [BlueSky](https://bsky.app/profile/bunny.net) * [Discord](https://discord.gg/bunnynet) ### 4. Email your friends or mailing list Got a newsletter or some tech-savvy friends? Send a friendly heads-up about your favorite new tool. * **How:** Keep it personal and helpful, like a tip you'd give your best friend. * **Pro tip:** Include a quick story or example of how bunny.net helped you. * **Example email snippet:** *"Hey! I found a service that made my site way faster. It's called bunny.net, super easy to use, and crazy fast. Thought you might want to try it too: \[your link]."* ### 5. Join the conversation in online communities Reddit, Discord, Facebook groups, and forums are places where people talk about tech, streaming, or websites. Wherever that happens, there's an opportunity. * **How:** Jump into threads where someone's struggling with slow load times or video issues. Share what worked for you. * **Pro tip:** Be genuinely helpful. That builds trust (and clicks). * **Example comment:** *"Totally had the same issue. bunny.net helped me drop my load time from 6s to under 2s. Might be worth a try: \[your link]."* **Mix and match!** You don't have to be everywhere. Just pick 1-2 channels where you feel comfortable and start there. Track what works and scale from there. *** ## Step 3: Nail your message No matter where you're sharing, blog, video, tweet, or TikTok, it's not just *what* you say, it's *how* you say it. That's what makes people click. Here's how to make your message land: ### Be real People can spot a sales pitch from a mile away, but they'll listen to a real story. * **Share your journey:** What problem did you face, and how did bunny.net help? * **Keep it casual:** Write like you're texting a friend, not writing an ad. **Example:** *"My videos used to buffer all the time. Switched to [bunny.net](https://bunny.net/) and haven't had issues since!"* ### Make the value shine Make it clear what's in it for them. Highlight real benefits, like: * Speeding up websites * Buffer-free video streaming * Reliable, low-latency file storage * Tools developers actually enjoy using **Try this:** *"Need a faster site without the tech headache? bunny.net is made for that."* ### Add a natural call-to-action Guide your audience toward action, but do it like a helpful friend, not a pushy salesperson. Swap this: *"Click this amazing link now!"* For this: *"Want to speed up your site too? Check out what worked for me." or this "Give it a try. I think you'll love how smooth your site gets."* ### Use numbers (they're powerful) People love results. If you have real improvements, share them! * *"Cut my load time by 3 seconds."* * *"Pages now load 60% faster."* * *"My bounce rate dropped by 25% after switching to bunny.net."* **Example Post:** *"It's faster, costs less, and it's quick and easy to set up. If you're interested, here's my link."* *** ## Step 4: Track your progress You've put your bunny.net link out into the world, nice work! Now it's time to see how it's doing. Just hop over to the **Affiliate** section in your bunny.net dashboard, and you'll find all the juicy stats you need: * **Referrals:** See how many people clicked your link and signed up. * **Rewards:** Track how much carrot power you've racked up! * **Payouts:** Request a payout or add your affiliate carrots to your Bunny balance. *** ## Step 5: Experiment, test, and have fun! The best part? You don't need to get it perfect on the first try. In fact, trying new things is half the fun and the secret to long-term success. Here's how to keep things fresh and effective: * **A/B test** Try two versions of a tweet, video title, or blog headline. See which one gets more clicks, and learn what your audience loves! * **Engage with your people** Got questions or comments? Drop a reply, say thanks, or ask for feedback. Building trust = more conversions. * **Stay consistent** Share often! A quick tip today, a story next week, a follow-up video later, it all adds up. **Example ideas to test:** * A side-by-side "before and after" speed test video * A tweet thread breaking down "How I fixed my slow website in 3 steps" * An Instagram Reel showing your setup with Bunny Stream in action *** ## That's a wrap! With these steps under your belt, you're ready to hop into the Bunny Affiliate Program with confidence. Remember, it's not about being perfect, it's about being *genuine*, helpful, and a little creative. So go ahead. Share your story, track your progress, try new things, and have fun while you earn! **P.S. Need help?** We've got your back! Just reach out to us anytime at [affiliate@bunny.net](mailto:affiliate@bunny.net), and we'll be happy to help. Happy hopping! # Affiliate Guidelines Source: https://bunny.net/docs/billing/affiliate-guidelines Guidelines for promoting bunny.net through the Bunny Affiliate Program. # Welcome to the Bunny Affiliate Program! We're thrilled to have you hop on board, spread the Bunny word (the right way!), and help more people discover a faster, smarter, and simpler way to deliver content at the edge. ## 1. Keep it real People relate to real stories, not sales talk, so share what your experience with bunny.net has actually been like. What made you hop on? What surprised you? What convinced you to stick with us? Share that. Keep it honest, personal, and grounded in what actually makes Bunny awesome for you. No need to overpromise. Your genuine take is what will resonate with everyone most. **Example:** "bunny.net will make your site the fastest in the world!" "What I love most about [bunny.net](https://bunny.net/) is that it just works. I don't think about it, which is exactly what I want from my infrastructure." "I'm not a CDN expert, but setup with [bunny.net](https://bunny.net/) took me maybe 10 minutes. It was really easy." ## 2. Share with care, not clutter Nobody likes a random link drop. Don't spam forums or stuff comment sections with your affiliate code just to chase clicks. That's not how we hop around here. Instead, look for moments where someone's stuck: a slow site, scaling issues, or delivery pains, and share how Bunny helped you solve something similar. Real help, real stories, real value. That's what builds trust. And that's what gets your link the right kind of attention. **Example:** Dropping your link where it doesn't belong? That's a fast track to losing trust. Someone asks how to speed up their website? That's a perfect moment to mention bunny.net and share your link! ## 3. Keep it in kind corners of the internet Bunny is built to make the internet hop faster, safer, and more fun. So let's keep that spirit in where and how you share. Please don't promote your affiliate link on sites that spread harm, hate, or anything illegal. That's not what we're about. Stick to spaces that reflect the same values we share: honesty, creativity, and helping others hop ahead. **Example:** No links on sites promoting hate speech, violence, or illegal activities. Share your link where it can truly help, like on your blog, in dev communities, or your social media channels. ## 4. Play by the platform's rules Every platform has its own playbook, and part of earning trust is respecting it. If Instagram wants a #ad, add it. If a forum says no affiliate links, don't try to sneak one in. Clear, honest, and upfront is how Bunny rolls. Transparency builds credibility and makes your recommendations go even further. **Example:** Disclosing your affiliate links builds trust and shows you play by the rules, which is exactly how our Bunnies hop. ## 5. Show off the Bunny brand (the right way) Our brand is part of what makes Bunny special. So let's keep it consistent and unmistakably Bunny. Feel free to use [official bunny.net logos and visuals](https://www.figma.com/design/hXVOrM9Jg4IlVw3femLqXr/Bunny-Promo-Assets?node-id=0-1\&t=7ZeexKH5n0nUH4dm-1), or create your own assets that follow our brand guidelines. Just don't copy content from other affiliates or slap together stuff that doesn't feel like Bunny. We want your creativity to shine within the lines that keep our brand strong. **Example:** Don't copy a competitor's logo and slap it on your post. [Grab Bunny-approved banners from the library.](https://www.figma.com/design/hXVOrM9Jg4IlVw3femLqXr/Bunny-Promo-Assets?node-id=0-1\&t=7ZeexKH5n0nUH4dm-1) ## 6. Be upfront: trust starts there People appreciate honesty, especially online! So when you're sharing an affiliate link, make it crystal clear. Not just because it's often legally required (which it is), but because it shows respect for your audience. **Example:** "This is an affiliate link, which means I may earn a commission if you sign up." ## 7. Share the Bunny story. No need to throw shade. We're proud of what we've built, and there's no need to tear others down to lift Bunny up. Talk about what Bunny does well, not what others don't. Focus on what makes Bunny awesome for you and let that speak for itself. **Example:** "Other CDNs are slow and expensive. Use bunny.net instead!" "I switched to bunny.net because it's fast, affordable, and super easy to use!" ### Questions? Not sure about something? Just ask us at **[affiliate@bunny.net](mailto:affiliate@bunny.net).** We're here to help! *** ### By joining, you agree to: * Share bunny.net responsibly * Use your affiliate link with honesty and care * Follow these simple guidelines * Promote us in ways that are ethical, kind, and professional We're super excited to have you as part of the Bunny Affiliate crew! By following these simple guidelines, you'll not only **earn rewards** but also help us keep Bunny's reputation strong and trustworthy. If you have any questions, we're just an email away! # Affiliate Program Source: https://bunny.net/docs/billing/affiliate-program Now you can earn carrots for helping others hop faster too. You know Bunny. You love Bunny. Now you can earn carrots for helping others hop faster too. Sign up, get your unique link, and share it with your friends, followers, or fellow devs. When they hop on and become paying customers, you get rewarded. No complicated rules. Just a simple way to turn your Bunny experience into real wins (for them *and* for you). ### Who is eligible for the Bunny Affiliate Program? If you're a paying [bunny.net](https://bunny.net/) customer, you're in! The program is open to anyone already hopping with us. ### How do I join the Affiliate Program? Easy! Just log in to your [bunny.net](https://bunny.net/) account, click your profile icon (top left), and hop to the Affiliate Program page. Enable it with one click, grab your unique link, and you're ready to start sharing. ![image.png](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/d0eecc5ca1254401c2d315e3a2a0c42816a9d969f0b51ad3db049f1c70a7dcb6-image-106.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=5967939356dccb3fb09b98a2de945af2) ### How much do I get for every person I refer? You earn **\$20** for every new paying customer who signs up through your link. Simple as that. Your rewards and referrals live in the Affiliate tab. Easy to find and easy to follow. ### Is there a limit on how much commission I can earn? Not at all. There's no cap on how much you can earn. Every new paying customer who signs up through your link means more rewards in your warren. Keep sharing, keep earning, and watch the carrots roll in. ### When can I expect my payout? Anytime you're ready. Just hop into your Affiliate page, request a payout, and choose the method that works in your region. Easy. Want even more bounce for your buck? You can turn your rewards into Bunny credits and get a **25% bonus!** Perfect for powering up your next project. Claim \$20, get \$25. Just like that. ### Can I create my own marketing materials? Yes please! We love seeing your creativity hop to life! You're free to design your own assets, write your own copy, or build your own banners. Just make sure everything follows our brand [guidelines](/docs/billing/affiliate-guidelines) so it still feels unmistakably Bunny. Need a head start? We've got [a stash of official logos, graphics, and goodies](https://www.figma.com/design/hXVOrM9Jg4IlVw3femLqXr/Bunny-Promo-Assets?node-id=0-1\&t=7ZeexKH5n0nUH4dm-1) ready for you to grab and go. ### How do I track my referrals and earnings? Curious how your link's performing? Hop into your dashboard and head to the Affiliate page. You'll see every click, signup, and successful referral, plus a clear view of your earnings and past payouts. All the good stuff, in one place. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/96841d2d42fc11df63cdfe6c67c30bb6abd5633a6ca55358bd2ca7a410cf1c61-image-107.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=bb5ea0ecc66e04553f1ea63c79da875f) ### What is considered a valid referral? It starts with your link. Someone clicks it, signs up for [bunny.net](https://bunny.net/), and gives it a go. Once they become a paying customer, boom. That's a valid referral. And just like that, your reward is on the way. ### What are the rules for promoting my affiliate link? We're all for creativity. In fact, we love seeing the unique ways you share Bunny. Just keep it kind, clear, and honest. No spam. No shady shortcuts. And no aggressive ads. Here's the rule of thumb: share the way *you'd* want someone to share with you. For more tips on what's cool (and what's not), check out the full [Affiliate Guidelines](/docs/billing/affiliate-guidelines). ### How do payouts work? Once you've earned rewards, you've got two ways to collect your carrots. **Want instant credit?** You can convert your affiliate balance into Bunny credits anytime. No minimum required. We'll even boost it with a **25% bonus**. Add \$20 and we'll drop \$25 into your account. Just like that. **Prefer a monetary payout?** You'll need at least **\$100** in rewards before requesting one. Once you hit the threshold, just hop over to your Affiliate Program page to request your payout. We send monetary payouts through our partner [Tremendous](https://www.tremendous.com/). They offer a range of methods, such as PayPal or a virtual VISA card, and the ones you can pick depend on where you are. So does the currency you get paid in. Tremendous checks your location when you redeem your reward, then shows the options it supports there. To see what your region gets, read [Sending international rewards](https://help.tremendous.com/hc/en-us/sections/41475572352275-Sending-international-rewards). > [!WARNING] > > Tremendous cannot deliver a reward to every recipient. Certain limitations apply on their side, so check [Tremendous recipient restrictions](https://help.tremendous.com/hc/en-us/articles/41472341152019-Understand-recipient-restrictions) before you count on a monetary payout. > > If a monetary payout isn't available to you, you can still convert your balance into Bunny credits. ![image.png](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/dc2d0bb69c08ac5d16de694048a6a89aa13d0fd1ce0cd528e97e73f6f8dfac86-image-108.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=1326a47c77a0b9150faa7e6c84952851) ### Can I participate from any location? Yes! Whether you're coding from São Paulo, designing in Tokyo, or building dreams in the Swiss Alps, you're welcome to join. If [bunny.net](https://bunny.net/) works in your country, you're good to go. The Affiliate Program is open worldwide, so no matter where you hop in from, you can share, earn, and be part of the Fluffle. Collecting a monetary payout is a slightly different story. Those go through our partner Tremendous, so the options you get depend on your region, and certain limitations apply on their side. Converting your balance into Bunny credits works everywhere, so there's always a way to spend what you earn. See [How do payouts work?](#how-do-payouts-work) before you count on a monetary payout. # Auto-Recharge Source: https://bunny.net/docs/billing/auto-recharge Automatically add funds when your balance runs low. Auto-Recharge automatically tops up your account when your balance gets low or is about to expire. Once enabled, you never have to worry about manually adding funds. You can enable Auto-Recharge from the [Billing](https://dash.bunny.net/account/billing) page. ![Auto-Recharge](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/enable-auto-recharge.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=d80e5e36fd4ead89977cc4e4dfc2b8cb) ## How it works bunny.net operates on a prepaid model where credits are deducted from your balance as you use services. Auto-Recharge triggers when your balance reaches your recharge threshold, automatically charging your saved payment method for the amount you configure. The threshold is 20% of your recharge amount. If your balance is already negative, the payment also covers the negative amount. See [Negative Balance](/docs/billing/negative-balance). ## Setting up Auto-Recharge **1. Go to Billing** Navigate to [Billing](https://dash.bunny.net/account/billing) in your dashboard. **2. Click Enable Auto-Recharge** Click the **Enable Auto-Recharge** button. **3. Choose the amount** Select a recharge amount: \$10, \$25, \$50, \$100, \$250, \$500, \$1000, \$2000, or enter a custom amount. **4. Select a payment method** Choose from your saved payment methods. To add a new payment method, use it first to manually recharge your account. **5. Confirm** Click **Set up Auto-Recharge** to enable. ## Supported payment methods Auto-Recharge supports Mastercard, Visa, American Express, and PayPal. ## Troubleshooting ### My Auto-Recharge payment didn't trigger To prevent excessive retries, Auto-Recharge only triggers every six hours. Your balance might temporarily appear below the threshold without a payment triggering. Wait a couple of hours to see if the payment executes. If your balance continues to drop after 24 hours with no payment, the Auto-Recharge payments are likely failing. This could be due to: * Credit card authorization failure * Insufficient balance on the card * Rejected payments * Expired credit card We only retry a card up to 5 times. After consecutive failures, you'll need to trigger a payment manually. ### Fixing failed Auto-Recharge Try removing and re-adding your saved payment method. This re-authorizes the card for automatic charges. If you're still unable to make a payment, [contact support](https://bunny.net/contact). # Billing History & Invoices Source: https://bunny.net/docs/billing/billing-history View past transactions and download receipts, monthly invoices, and usage summaries. The [Billing History](https://dash.bunny.net/account/billing-history) page shows all your account transactions, including monthly usage, payments, and applied promo codes. ![Billing history](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/accounts-and-billing/billing-history.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=67e072f2909f81fdff242ae574494370) ## What you'll see The billing history displays: * **Date** of the transaction * **Description** of the charge or credit * **Amount** * **Type** (Monthly usage, PayPal, Coupon code, etc.) A chart at the top shows your account balance over time. ## Receipts and invoices bunny.net issues two kinds of billing documents: * **Receipts** confirm a payment. You get a receipt each time you add funds, either manually or through [Auto-Recharge](/docs/billing/auto-recharge). * **Invoices** are issued once a month and cover the previous month's usage. Tax is charged on usage, not on the funds you add, so any VAT or sales tax appears on your monthly invoice. See [Taxation](/docs/billing/taxation). > [!NOTE] > > Adding funds does not create an invoice. For your accounting records, use the > monthly invoice for the month in which the usage happened. ## Downloading receipts For payment entries, click **Download receipt** to get a PDF receipt. We also email the receipt to you after each payment. ## Downloading monthly invoices At the start of each month, we issue an invoice for the previous month's usage and email it to you. To download it, click **Download invoice** on the monthly usage entry for that month. No invoice is issued for a month with no charges. ## Downloading usage summaries For monthly usage entries, click **Download summary** to get a detailed breakdown of your charges for that period. The usage summary includes: * Your billing details * Starting and ending balance * Total activity, credits, payments, and refunds * Itemized usage by service (traffic by region, storage, scripting, etc.) > [!NOTE] > > Usage summaries are not invoices. They are a detailed breakdown of billing > activity for the stated period. # Negative Balance Source: https://bunny.net/docs/billing/negative-balance What happens when your bunny.net balance reaches zero, which charges continue, and how to restore your account. bunny.net is a prepaid service. Charges are deducted from your balance as you use services. If your balance runs out, we keep charging for usage and your balance becomes negative. You remain responsible for all charges on your account, including those made while the balance is negative. > [!NOTE] > > Enable [Auto-Recharge](/docs/billing/auto-recharge) to top up automatically before > your balance runs out. ## Why a balance can go negative Usage is billed after it happens, in short intervals. If you use more between two billing runs than your remaining balance covers, the balance goes below zero. ## Charges that continue Some charges don't depend on traffic. They continue while your balance is negative and while your account is disabled, for as long as the resource exists or the feature is enabled: * Storage zones and Stream video storage, billed per GB stored * Magic Containers persistent volumes and attached Anycast IPs * Database storage * The [\$1 minimum monthly usage](/docs/billing/add-funds#minimum-monthly-usage) To stop these charges, delete the resources or disable the features you no longer need. ## When your balance reaches zero 1. **You are notified.** We email you daily while your balance is negative, and the dashboard shows a warning. 2. **Auto-Recharge runs** if you have it enabled. The payment covers your recharge amount plus the negative amount, so your balance returns to the amount you chose. 3. **You can't create new resources** until your balance is positive again. 4. **Your account is disabled** if the balance is not topped up within 4 days of going negative. We may disable an account sooner, for example for new accounts or large negative amounts. ## When your account is disabled All your services go offline. Your files and configuration are kept, and you can still log in to the dashboard to top up. Usage charges stop because the services stop. The [charges listed above](#charges-that-continue) continue, so your balance can keep falling while the account is disabled. ## Restoring your account Top up enough to bring your balance to zero or above. Your account and services are re-enabled immediately. The negative amount is deducted from the top-up. For example, with a balance of -\$4, a \$10 top-up leaves \$6. ## Data deletion If your account stays disabled for 60 days, your stored data is permanently deleted. This includes storage zones, video libraries, and Magic Containers apps and volumes. Deleted data can't be recovered. We send several warning emails before deleting your data, starting 30 days before the deletion date. # Sales Tax Source: https://bunny.net/docs/billing/sales-tax Information about U.S. sales tax on bunny.net services. > [!NOTE] > > All prices in our dashboard and on our website are shown without any tax. We apply U.S. sales tax on digital services when required by the customer's state and local laws. This affects only customers in states or localities that tax digital services. U.S. sales tax on digital services is not consistent across the country. Some states tax digital services, others do not, and some apply tax only in specific cases. Rules and rates also change over time. ## What this means for your invoices * **Only the tax line may change.** Your service prices stay the same. * **Tax depends on your billing address.** If your billing address is in a state or locality that taxes digital services, sales tax will appear on the invoice. If not, you will not be charged sales tax. * **Rates can change.** States update digital service tax rules and rates regularly. We apply the latest available rules and rates at the time of invoicing. ## How we determine your tax We calculate sales tax based on: * **Your billing address state and locality.** U.S. sales tax is mainly state based, and many states allow local add-ons. * **How that state taxes digital services.** Every state defines digital service taxability differently. * **Your usage for the billing period.** Tax is applied to the taxable part of your invoice based on what you used. Because rules vary, two customers with the same usage can see different tax results if they are in different states. ## Changing your billing address You can [change your billing address](/docs/account#billing-address) once per month. If you update your address, the new tax jurisdiction and rate take effect on the first day of the next month. We do not change tax rates mid-month because invoices must reflect one jurisdiction per billing period. This is a common compliance requirement. ## Tax exemptions If your organization is tax exempt, send us a valid state exemption certificate. After verification, we will stop charging sales tax on future invoices as required by that state. > [!NOTE] > > Exemptions are state specific. The certificate must match the state on your > billing address. We cannot remove tax retroactively unless state rules allow > it and your certificate covered that period. # CDN Acceleration Source: https://bunny.net/docs/cdn/cdn-acceleration Connect your domain to a Pull Zone using Bunny DNS for automatic CDN routing and simplified SSL. CDN Acceleration routes your domain's traffic through the Bunny CDN automatically when using Bunny DNS. Static assets (images, CSS, JS, videos) are served from Bunny's global network without manually configuring Pull Zones for each subdomain. ## Benefits * Connect your domain to the CDN with a few clicks * Automatic SSL via Let's Encrypt (no manual certificate setup) * Reduced load on your origin server * Works with root domains (no CNAME limitation) ## Prerequisites Your domain must use Bunny DNS. Update your domain's nameservers at your registrar to: * `coco.bunny.net` * `kiki.bunny.net` ## Setup **1. Open your DNS zone** 1. Go to **DNS** in your [bunny.net dashboard](https://dash.bunny.net) 2. Click on the domain you want to accelerate **2. Enable CDN Acceleration** 1. Find the **CDN Acceleration** button (or CDN Proxy column) 2. Click **Enable** Bunny automatically creates a Pull Zone and configures SSL for your domain. > [!WARNING] > > If you have an existing Pull Zone with the same hostname, delete that hostname from the Pull Zone first. Hostnames can only be mapped to one Pull Zone at a time. **3. Verify it's working** Visit your site. Static assets should now be served through bunny.net's CDN. **Dashboard** The green **CDN Proxy** icon next to your DNS record confirms successful setup. You can also check **Monitoring** > **Logs** and filter by your accelerated pull zone to confirm requests are being logged. **HTTP headers** Open your browser's Developer Tools (**F12** or right-click > **Inspect**), go to the **Network** tab, and refresh the page. Click on a static file (`.jpg`, `.css`, `.js`) and check for these headers: | Header | Description | | - | - | | `cdn-pullzone` | The Pull Zone handling the request | | `cdn-cache` | Cache status (`HIT`, `MISS`, or `BYPASS`) | | `cdn-requestcountrycode` | Country from which the request was served | If these headers are present, your content is being served through bunny.net's CDN. ## SSL certificate timing Bunny issues SSL certificates via Let's Encrypt within seconds of enabling acceleration. If HTTPS doesn't work immediately, wait a few minutes for the certificate to propagate. > [!NOTE] > > Ensure your nameservers point to Bunny DNS before enabling acceleration. If > the domain isn't resolving through Bunny yet, SSL certificate verification > will fail. # Connectivity Source: https://bunny.net/docs/cdn/connectivity Learn how bunny.net supports IPv4 and IPv6 connectivity across our global network. bunny.net supports both IPv4 and IPv6 connectivity across our global edge network. Our IPv6 support continues to expand as we upgrade connectivity throughout our infrastructure and partner networks. ## Network Support bunny.net operates two network tiers with different IPv6 support levels: | Network | Coverage | IPv6 Support | | - | - | - | | Standard Network | 119+ Points of Presence | Best effort | | Volume Network | 10 Points of Presence | Native dual-stack | The **Volume Network** is fully dual-stack, providing native IPv4 and IPv6 connectivity throughout the entire delivery path. The **Standard Network** offers IPv6 support on a best-effort basis. While many locations already support dual-stack connectivity, some edge locations remain IPv4-only due to upstream network limitations or regional infrastructure constraints. ## CDN Ingress Connectivity When users connect to bunny.net, DNS responses are generated based on the capabilities of the serving location. * **A records** are always available for IPv4 connectivity. * **AAAA records** are returned when the serving Point of Presence supports IPv6 connectivity. * Clients with IPv6 connectivity will automatically connect over IPv6 whenever possible. This approach ensures optimal compatibility while allowing IPv6-capable locations to serve traffic natively over IPv6. Each Pull Zone can override this default behavior with an [IP Family Policy](/docs/cdn/ip-family-policy), which controls whether we advertise IPv4, IPv6, or both, and whether routing prefers IPv6-capable locations. ## Origin Connectivity We support both IPv4 and IPv6 connections to your origin servers. You can configure origins using: * Hostnames that resolve to **A records** * Hostnames that resolve to **AAAA records** * Dual-stack hostnames that resolve to both **A and AAAA records** * Direct IPv6 addresses enclosed in square brackets (`[]`) according to RFC standards Examples: ```text theme={null} https://origin.example.com https://[2001:db8::1] ``` When a hostname resolves to both IPv4 and IPv6 addresses, we will attempt to establish connectivity using the best available path based on the capabilities of the serving edge location. ### Volume Network Origins The Volume Network provides native dual-stack connectivity to origins. Edge servers within the Volume Network can establish connections to both IPv4 and IPv6 origin infrastructure. ### Standard Network Origins On the Standard Network, IPv6 origin connectivity is provided on a best-effort basis. If the serving edge location has IPv6 connectivity to the origin, we can connect to IPv6-enabled origins. Otherwise, IPv4 connectivity will be used. If guaranteed IPv6 connectivity to your origin is required, enabling Origin Shield routes all origin traffic through a Volume Network location with native dual-stack support. ## 502 and 504 errors A `502 Bad Gateway` means an edge server received an invalid response from your origin. A `504 Gateway Timeout` means the origin didn't respond in time. When the origin is unreachable, edge servers serve from cache where possible and only return the error when the file isn't cached. Common causes: * **Origin offline:** The most common cause. Edge servers can't connect to your origin and the file isn't in cache. * **Firewall blocking edge servers:** Because the CDN proxies all your traffic, origin firewalls or security software can mistake the volume of edge requests for an attack and block them. Whitelist the [edge server IP list](https://api.bunny.net/system/edgeserverlist) ([IPv6](https://api.bunny.net/system/edgeserverlist/ipv6)) and update it periodically as the network changes. * **Network congestion:** A rare, transient routing problem between an edge location and a distant origin. * **Timeouts:** By default Bunny waits 10 seconds for a TCP connection and 60 seconds for the origin to start sending data. Immediate 502s usually mean an origin firewall or WAF is rejecting the connection. These timeouts are configurable via Safehop. Use the [bunny.net diagnostic tools](https://tools.bunny.net/) to test latency, traceroutes, and HTTP requests from over 120 locations and see whether specific regions are blocked or the origin is slow. If you've ruled out the causes above, contact [support@bunny.net](mailto:support@bunny.net). ## Origin redirect loops If your CDN URLs return a 301 redirect back to your own domain, the origin is issuing that redirect and the CDN is forwarding it. Bunny CDN never returns a 301 on its own, except for hotlink protection. Check the following: * **Origin URL host:** Make sure the Pull Zone Origin URL matches the exact domain your site serves from, including the `www` or non-`www` version. A mismatch triggers your origin's canonical redirect. * **Origin URL protocol:** If your server forces HTTPS, set the Origin URL to `https://`. An `http://` Origin URL against an HTTPS-only origin produces a redirect. * **Origin redirect or rewrite rules:** Check your server configuration for redirect or rewrite rules affecting your assets. * **Hotlink protection:** If enabled, whitelist every domain the CDN serves (both `www` and non-`www`), with no protocol or slashes in the hostname field. After any change, purge the full Pull Zone cache and wait for it to sync, since cached 301s persist on the edge. ## CDN Acceleration and IPv6 Origins bunny.net's CDN Acceleration feature supports dual-stack origin connectivity when used with Bunny DNS. When accelerating DNS records, we automatically detect matching IPv4 and IPv6 records for the same hostname and use both when establishing connectivity to your origin. For example, if your DNS zone contains: ```dns theme={null} www A 203.0.113.10 www AAAA 2001:db8::10 ``` and the **A record** is configured for CDN Acceleration, we will automatically discover the matching **AAAA record** and attempt to use both addresses when connecting to your origin. This enables: * Dual-stack origin connectivity through CDN Acceleration * Automatic IPv6 support without additional configuration * Improved resiliency by allowing both IPv4 and IPv6 paths to be used when available As with standard origin connectivity, IPv6 connectivity depends on the capabilities of the serving edge location. Volume Network locations provide native dual-stack connectivity, while Standard Network locations provide IPv6 connectivity on a best-effort basis. > [!NOTE] > > When accelerating a hostname through Bunny DNS, matching A and AAAA records for the same hostname are automatically considered for origin connectivity. You do not need to separately accelerate both record types. ## Origin Shield and IPv6 Origins For customers using the Standard Network who require guaranteed IPv6 connectivity to their origin infrastructure, we recommend enabling Origin Shield. Both Origin Shield locations: * **Chicago, IL** * **Paris, France** are part of the Volume Network and provide native dual-stack connectivity. When Origin Shield is enabled, all origin fetches are routed through the selected shield location rather than directly from the serving edge location. This ensures that communication between bunny.net and your origin always originates from a Volume Network Point of Presence with native IPv4 and IPv6 support. This configuration is particularly useful when: * Your origin is reachable only over IPv6 * You want to guarantee IPv6 connectivity regardless of which Standard Network edge location serves the request * You want to combine the global coverage of the Standard Network with reliable dual-stack origin connectivity > [!NOTE] > > Origin Shield can be used to guarantee IPv6 connectivity to your origin even when requests are served from Standard Network locations that do not currently have IPv6 connectivity to the origin. ## IPv6 Expansion Expanding IPv6 support remains an ongoing priority across our network. A significant portion of our infrastructure already operates in dual-stack mode, to achieve full IPv6 coverage across more than 119 Points of Presence we are actively coordinating with upstream providers and local network operators worldwide. Some locations currently operate in environments where native IPv6 connectivity is not yet available. As connectivity improves, IPv6 support will continue to expand across additional edge locations without requiring any changes to your CDN configuration. > [!NOTE] > > IPv6 support on the Volume Network is available across all locations. On the Standard Network, IPv6 availability depends on the capabilities of the serving Point of Presence and its upstream connectivity. # Custom 404 Page Source: https://bunny.net/docs/cdn/custom-404-page Display a branded error page when files are not found in your storage zone. When using a Storage Zone as your Pull Zone origin, you can configure a custom 404 page to display when requested files don't exist. ## Setup **1. Create the error folder** In the root of your storage zone, create a folder named `bunnycdn_errors`. **2. Upload your 404 page** Create a file named `404.html` with your custom HTML content and upload it to the `bunnycdn_errors` folder. To return an image instead, upload a file named `404.png` and remove any `404.html` file. **3. Verify** Navigate to a non-existent URL on your Pull Zone to confirm your custom 404 page appears. ## Folder structure ``` /your-storage-zone/ └── bunnycdn_errors/ └── 404.html ``` > [!NOTE] > > This feature only works when your Pull Zone origin is a Bunny Storage Zone. > For external origins, configure 404 handling on your origin server. # Custom Hostname Source: https://bunny.net/docs/cdn/custom-hostname Serve content from your own branded domain like cdn.yourdomain.com. Use a custom hostname to serve CDN content from your own domain instead of the default bunny.net URL. **1. Add the hostname to your pull zone** 1. Open your pull zone in the [dashboard](https://dash.bunny.net) 2. Locate the **Hostnames** panel in the General section 3. Enter your custom domain (e.g., `cdn.yourdomain.com`) 4. Click **Add hostname** ![Add custom hostname](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/add-a-custom-hostname.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=a0a71b2b488354d72247c1aa33587041) The hostname appears in the Linked Hostnames list. **2. Copy the CNAME record** Copy the CNAME record value shown below the hostname field. You'll need this for the next step. ![Copy custom hostname CNAME](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/copy-custom-hostname-cname.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=011e53191b962ce52d18b39a0c00bbb5) **3. Update your DNS settings** Add a CNAME record at your DNS provider: 1. Log in to your DNS provider (Cloudflare, Route 53, or similar) 2. Create a new **CNAME** record: * **Name/Host**: Your subdomain (e.g., `cdn`) * **Type**: CNAME * **Value**: The CNAME from step 2 (e.g., `yourzone.b-cdn.net`) * **TTL**: 3600 seconds or lower > [!WARNING] > > On Cloudflare, disable the proxy option (orange cloud icon). Proxying interferes with DNS resolution and prevents your custom domain from working with bunny.net. **4. Wait for DNS propagation** Save the DNS record. Your domain connects to bunny.net once DNS propagates, typically within a few minutes to an hour. **5. Enable SSL** Click **Verify & Activate SSL** to get a free Let's Encrypt certificate for your custom hostname. ![Verify and activate SSL](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/cdn/verify-and-active-ssl-custom-hostname.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=f8928d71336f40148362bcaf795a2e05) > **Enable SSL later** > > If you skip SSL verification initially, find your hostname in the **Linked Hostnames** section and click **Enable** to activate a free certificate or upload your own. > > ![Enable custom hostname SSL](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/enable-custom-hostname-ssl.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=27c082435b29c9805f4af5fa9eb6b5ad) ## Root domains CNAME records cannot be created at the apex level (`yourdomain.com`) per the DNS specification. Most DNS providers don't support non-standard record types like ALIAS or ANAME that work around this limitation. > [!WARNING] > > Pull Zones do not support direct IP address connections. There is no stable > anycast IP that can be used with A records to point a domain to a Pull Zone. > You can use a direct IP as the origin with a custom Host header, but pointing > an A record to a specific Bunny IP is not supported. See [How to set up a > direct IP origin URL](/docs/cdn/edge-rules/ip-origin) for more information. To use a root domain with your Pull Zone, you have two options: 1. **Use Bunny DNS (recommended)**: Bunny DNS supports CNAME flattening on apex domains, allowing you to point your root domain directly to a Pull Zone. See [CDN Acceleration](/docs/cdn/cdn-acceleration) for setup instructions. 2. **Redirect apex to www**: Create a CNAME for `www.yourdomain.com` pointing to your Pull Zone, then set up an HTTP redirect from your root domain to `www`. # Custom Cache Time for File Extensions Source: https://bunny.net/docs/cdn/edge-rules/custom-cache-time Use Edge Rules to set custom cache durations for specific file types. Edge Rules make it easy to customize cache times based on file extensions. This is useful when different content types need different caching strategies—for example, caching video files longer than HTML pages. ## Use cases * Cache static assets (images, CSS, JS) for longer periods * Set shorter cache times for dynamic content * Cache video files for extended periods to reduce origin load * Override default cache behavior for specific file types ## Configuration **1. Open Edge Rules** Navigate to your pull zone and select **Edge Rules** from the side menu. **2. Add a new rule** Click **Add Edge Rule** to create a new rule. **3. Select the Override Cache Time action** Choose **Override Cache Time** as the action. Enter the desired cache time in seconds: | Duration | Seconds | | - | - | | 1 hour | `3600` | | 1 day | `86400` | | 1 week | `604800` | | 1 month | `2592000` | **4. Add a File Extension condition** Click **Add Condition** and select **File Extension** as the condition type. Enter the extension you want to match (without the dot). For example: * `mp4` for video files * `jpg` for JPEG images * `css` for stylesheets **5. Save the rule** Click **Save Edge Rule** to activate. ## Examples ### Cache video files for 30 days | Setting | Value | | - | - | | **Action** | Override Cache Time | | **Cache Time** | `2592000` (30 days) | | **Condition** | File Extension | | **Extension** | `mp4` | ### Cache images for 1 week | Setting | Value | | - | - | | **Action** | Override Cache Time | | **Cache Time** | `604800` (7 days) | | **Condition** | File Extension | | **Extension** | `jpg` | > [!NOTE] > > Create separate rules for each file extension, or use multiple conditions with > **Match Any** to apply the same cache time to multiple extensions. ### Bypass cache for HTML files To prevent caching of HTML files entirely: | Setting | Value | | - | - | | **Action** | Override Cache Time | | **Cache Time** | `0` | | **Condition** | File Extension | | **Extension** | `html` | > [!WARNING] > > Setting cache time to `0` will bypass the edge cache if the content is not > already cached. This increases origin load but ensures fresh content. ## Multiple extensions To apply the same cache time to multiple file types, you have two options: ### Option 1: Multiple conditions (Match Any) Add multiple **File Extension** conditions to a single rule and set the match type to **Match Any**: * Condition 1: File Extension = `jpg` * Condition 2: File Extension = `png` * Condition 3: File Extension = `gif` ### Option 2: Separate rules Create individual rules for each extension. This gives you more flexibility if you later need different cache times per extension. ## Cache time vs Browser cache time Edge Rules offer two cache-related actions: | Action | Description | | - | - | | **Override Cache Time** | Controls how long content is cached on bunny.net edge servers | | **Override Browser Cache Time** | Controls the `Cache-Control` header sent to browsers | You can use both together to have different cache durations at the edge vs in the browser. ## Related * [Smart Cache](/docs/cdn/smart-cache) - Default cache settings for your pull zone * [Trigger Path Setup](/docs/cdn/edge-rules/trigger-path) - Alternative way to match files using wildcards * [Rule Ordering](/docs/cdn/edge-rules/ordering) - Understand how multiple cache rules interact # Dynamic Variables Source: https://bunny.net/docs/cdn/edge-rules/dynamic-variables Complete reference for dynamic variables available in Edge Rule actions like redirects, header modifications, and origin changes. Edge Rules support dynamic variables that automatically adapt based on request data. These variables allow you to create flexible rules for redirects, header modifications, and origin changes. ## Supported actions Variable expansion works in the following Edge Rule actions: * **Redirect to URL** * **Change Origin URL** * **Set Request Header** * **Set Response Header** ## Variable syntax There are two variable syntaxes available: | Syntax | Format | Example | | - | - | - | | Basic | `{{variable}}` | `{{path}}` | | Advanced | `%{Collection.Key}` | `%{User.CountryCode}` | > [!NOTE] > > The `{{ variable }}` syntax automatically includes slashes where needed in > URLs. When using `%{Collection.Key}` syntax, you need to manually include > slashes in your URLs. ## Basic variables These simple variables are commonly used for general redirection and header logic: | Variable | Description | Example Output | | - | - | - | | `{{path}}` | Full request path including query string | `/videos/test.mp4?user=1` | | `{{hostname}}` | The hostname from the request | `test.b-cdn.net` | | `{{country_code}}` | Two-letter country code of the user's IP | `US` | | `{{query_string}}` | Query string only (without the `?`) | `user=1` | | `{{request_method}}` | HTTP method used | `GET`, `POST` | | `{{file_name}}` | Filename from the URL (last part after `/`) | `file.jpg`, `test.mp4` | ### Example: Redirect preserving path To redirect all requests from one domain to another while preserving the path: | Setting | Value | | - | - | | **Action** | Redirect to URL | | **Redirect URL** | `https://www.example.com{{path}}` | | **Status Code** | 301 | *** ## Advanced variable collections The advanced variable system provides detailed access to request data using the `%{Collection.Key}` syntax. ### RequestHeaders Access headers from the incoming HTTP request. | Example | Result | | - | - | | `%{RequestHeaders.Host}` | `videos.example.com` | | `%{RequestHeaders.User-Agent}` | Browser or client info | *** ### Query Access specific query parameters from the URL. For a URL like `/video.mp4?user=123`: | Variable | Result | | - | - | | `%{Query.user}` | `123` | > [!NOTE] > > Replace `KeyName` with your actual query parameter name. For example, if your > query is `?Myhash=123`, use `%{Query.Myhash}` to get `123`. *** ### Path Extract specific segments of the URL path. This is useful for complex routing or origin overrides. For the example path `/hello/world/bunny/eat/carrot.jpg`: | Variable | Output | Description | | - | - | - | | `%{Path.0}` | `hello` | First segment | | `%{Path.1}` | `world` | Second segment | | `%{Path.0-2}` | `hello/world/bunny` | Range from index 0 to 2 | | `%{Path.1-3}` | `world/bunny/eat` | Range from index 1 to 3 | | `%{Path.3-}` | `eat/carrot.jpg` | From index 3 to end | | `%{Path.1-}` | `world/bunny/eat/carrot.jpg` | From index 1 to end | | `%{Path.-1}` | `hello/world` | From start up to (but not including) index 1 | > [!NOTE] > > If the URL includes a query string and the path you extract doesn't alter the filename, the query string remains intact. For example, with `/hello/world/bunny/eat/carrot.jpg?query=something`, using `%{Path.1-}` returns `world/bunny/eat/carrot.jpg?query=something`. *** ### Url Access parsed components of the full URL. | Variable | Description | Example | | - | - | - | | `%{Url.Filename}` | Last part of URL (empty for folders) | `video.mp4` | | `%{Url.Extension}` | File extension | `mp4` | | `%{Url.Directory}` | Folder path only | `/videos/2024/` | | `%{Url.Hostname}` | Hostname from request | `cdn.mysite.com` | | `%{Url.Path}` | Full path and query | `/videos/video.mp4?x=1` | *** ### User Information about the user making the request. | Variable | Description | Example | | - | - | - | | `%{User.IP}` | End-user IP address | `203.0.113.10` | | `%{User.CountryCode}` | Country of the request | `DE` | *** ### Server Internal information about the server processing the request. | Variable | Description | Example | | - | - | - | | `%{Server.ZoneCode}` | Code of the serving zone | `NY` | | `%{Server.ID}` | Server ID | `9482` | *** ### Request Information about the HTTP request. | Variable | Description | Example | | - | - | - | | `%{Request.Method}` | HTTP method | `GET`, `POST` | | `%{Request.Path}` | Full URL path and query | `/videos/video.mp4?x=1` | | `%{Request.QueryString}` | Query string only | `x=1` | *** ## Practical examples ### Redirect non-www to www Redirect all requests from `domain.com` to `www.domain.com` while preserving the path: | Setting | Value | | - | - | | **Action** | Redirect to URL | | **Redirect URL** | `https://www.domain.com{{path}}` | | **Status Code** | 301 | | **Condition** | Request URL | | **Condition Value** | `*` | > [!WARNING] > > This rule must be added to the pull zone where `domain.com` is declared, not where `www.domain.com` is declared. > > If both hostnames are in the same pull zone, narrow the condition to `*://domain.com*` to only match the non-www version. ### Route requests based on path segment Route requests to different origins based on the first path segment: | Setting | Value | | - | - | | **Action** | Change Origin URL | | **Origin URL** | `https://%{Path.0}.example-backend.com/%{Path.1-}` | | **Condition** | Request URL | | **Condition Value** | `*/api/*` | This would route `/api/users/123` to `https://api.example-backend.com/users/123`. ### Add country code to request header Pass the user's country to your origin for geo-based logic: | Setting | Value | | - | - | | **Action** | Set Request Header | | **Header Name** | `X-User-Country` | | **Header Value** | `%{User.CountryCode}` | | **Condition** | Request URL | | **Condition Value** | `*` | ## Related * [Variable Expansion](/docs/cdn/edge-rules/variable-expansion) - Detailed syntax reference for variable collections * [Trigger Path Setup](/docs/cdn/edge-rules/trigger-path) - Configure trigger conditions correctly * [Middleware Scripts](/docs/scripting/middleware/overview) - For complex transformations beyond Edge Rules # Edge Rules Source: https://bunny.net/docs/cdn/edge-rules/index Create powerful custom behaviors for your CDN using Edge Rules to control redirects, headers, caching, and origin routing. Edge Rules allow you to create powerful custom behaviors for your pull zone, such as redirects, header modifications, cache time overrides, or origin changes based on incoming request data. ## How to access Edge Rules **1. Navigate to your Pull Zone** Go to **CDN** in the dashboard and select the pull zone you want to configure. **2. Open Edge Rules** Click on the **Edge Rules** tab in the pull zone settings. **3. Add a new rule** Click **Add Edge Rule** to create a new rule with your desired action and trigger conditions. ## What can Edge Rules do? Edge Rules support a variety of actions that execute at different layers of the CDN: | Action | Description | | - | - | | **Force SSL** | Redirect HTTP requests to HTTPS | | **Redirect to URL** | Redirect requests to a different URL | | **Change Origin URL** | Route requests to a different origin server | | **Override Cache Time** | Set custom cache duration for matching requests | | **Block Request** | Block requests that match certain conditions | | **Set Response Header** | Add or modify response headers | | **Set Request Header** | Add or modify headers sent to the origin | | **Force Download** | Force the browser to download the file | | **Disable/Enable Token Authentication** | Control token authentication per request | | **Override Browser Cache Time** | Set custom browser cache duration | | **Set Network Rate Limit** | Limit bandwidth for matching requests | | **Enable/Disable Request Coalescing** | Control request coalescing per request | ## Guides > **[Trigger Path Setup](/docs/cdn/edge-rules/trigger-path)** > > Learn how to correctly configure trigger paths with wildcards and pattern > matching. > **[Custom Cache Time](/docs/cdn/edge-rules/custom-cache-time)** > > Set custom cache durations for specific file extensions. > **[Redirect Hostname](/docs/cdn/edge-rules/redirect-hostname)** > > Redirect your b-cdn.net hostname to a custom domain. > **[Direct IP Origin](/docs/cdn/edge-rules/ip-origin)** > > Connect directly to an origin IP with a custom hostname. ## Reference > **[Dynamic Variables](/docs/cdn/edge-rules/dynamic-variables)** > > Complete list of variables available in Edge Rule actions. > **[Variable Expansion](/docs/cdn/edge-rules/variable-expansion)** > > Use variable expansion syntax to create dynamic rule values. > **[Rule Ordering](/docs/cdn/edge-rules/ordering)** > > Control execution order and understand rule priority. ## Need more power? For complex request and response transformations that go beyond what Edge Rules can offer, consider using [Middleware Scripts](/docs/scripting/middleware/overview). Middleware scripts allow you to write custom code that executes at the edge, giving you full programmatic control over request handling, response modification, and routing logic. # Direct IP Origin with Custom Hostname Source: https://bunny.net/docs/cdn/edge-rules/ip-origin Connect your pull zone directly to a server IP address while serving content from a custom hostname using Edge Rules. This guide explains how to connect your pull zone to your server IP directly, bypassing intermediary services (such as firewalls or proxies) that might block CDN access to your origin. This is useful when: * Your origin is behind a firewall that blocks CDN requests * You need to bypass proxy services like Cloudflare * You want to connect directly to a specific server IP ## Prerequisites * A pull zone configured in the bunny.net dashboard * Your origin server's IP address * A custom hostname you want to use for serving content ## Configuration **1. Update your origin URL to the server IP** Navigate to your pull zone and go to **General** → **Origin**. Set the origin URL to point directly to your server's IP address: * Select the correct scheme (`HTTP` or `HTTPS`) to prevent redirects * If you have **Forward Host Header** enabled, disable it for this configuration > [!NOTE] > > At this point, your pull zone will connect to the server directly. However, the server will return the default website configured for that IP. Step 2 configures bunny.net to send the correct hostname to your origin. **2. Add the custom hostname to your pull zone** Go to **General** → **Hostnames** and add your custom hostname: 1. Enter your domain name in the **Add a custom hostname** field 2. Click **Add Hostname** **3. Configure DNS** Update your DNS provider to point your custom hostname to the bunny.net pull zone: - Create a **CNAME** record pointing to your pull zone hostname (e.g., `your-zone.b-cdn.net`) **4. Enable SSL (optional)** After DNS propagation, you can enable SSL for your custom hostname: 1. Click **Verify & Activate SSL** to get a free Let's Encrypt certificate 2. Alternatively, scroll to **Linked Hostnames** and click **Enable** to activate SSL later or upload your own certificate **5. Create an Edge Rule to set the Host header** Navigate to **Edge Rules** and create a new rule to send the correct hostname to your origin: | Setting | Value | | - | - | | **Action** | Set Request Header | | **Header Name** | `Host` | | **Header Value** | Your origin hostname (e.g., `example.com`) | | **Condition** | Request URL | | **Match Type** | Match Any | | **Condition Value** | `*` | **6. Purge cache** If you had cached content from a previous configuration, purge the cache to ensure fresh content is fetched from the new origin. ## How it works When a request comes in: 1. The client connects to your custom hostname (served by bunny.net) 2. bunny.net connects directly to your origin server IP 3. The Edge Rule sets the `Host` header so your origin knows which website to serve 4. Your origin responds with the correct content This bypasses any intermediate proxies while maintaining proper hostname routing. ## Related * [Variable Expansion](/docs/cdn/edge-rules/variable-expansion) - Use dynamic values in your Edge Rules * [Rule Ordering](/docs/cdn/edge-rules/ordering) - Understand how multiple rules interact # Edge Rules Ordering Source: https://bunny.net/docs/cdn/edge-rules/ordering Control the sequence of your Edge Rules to fine-tune behavior and execution priority. ## How Edge Rules Are Ordered Edge Rules execute in the order defined by the **OrderIndex**. Rules are always processed from the smallest to the largest **OrderIndex** value: * A rule with `OrderIndex = 1` will execute **before** a rule with `OrderIndex = 2`. * All rules are executed sequentially based on their index. This allows you to control which rules should apply first, which is especially important when multiple rules affect the same headers or behaviors. ## Default Behavior: What Happens if No `OrderIndex` Is Set? If you do not specify an `OrderIndex` for your Edge Rules: * Rules will execute in **the order they were created**. Because of this, **we strongly recommend setting an explicit OrderIndex for all rules** to ensure predictable and consistent execution order. ## Short-Circuiting vs. Multi-Action Execution Edge rules process actions differently depending on the action type. Some actions follow a **short-circuiting model**, where **only the first matching action of that type is executed**, and further actions of that same type are ignored. This behavior applies when executing multiple actions of the same kind would cause conflicts or unintended side effects. Other actions support **multi-action execution**, meaning **all matching actions of that type are processed**, in sequence. This is typical for additive behaviors like setting response headers or appending cookies, where multiple values can coexist. We will add either the 🛑(`short-circuiting model`) or 🔁(`multi-action execution`) icon to the action list below to indicate how each action is processed. ### Example Behaviors If multiple rules modify the same request or response header, all matching actions will be executed, allowing cumulative changes. If multiple rules attempt to rewrite the URL or redirect, only the first matching action is executed, and subsequent conflicting actions are skipped. **Example:** | OrderIndex | Condition | Action | | - | - | - | | 1 | If `Request URL` equals `/images` | `Override Browser Cache Time` -> `3600` | | 2 | if `Request URL` equals `/images` | `SetResponseHeader` -> `hello=world` | | 3 | If `Request Header` -> `User-Agent` contains `Googlebot` | `Override Browser Cache Time` -> `5` | | 4 | if `Request Header` -> `User-Agent` contains `Google` | `SetResponseHeader` -> `hello2=world2` | If a request matches both conditions: * The `Cache-Control` value will be `3600`, because `Override Browser Cache Time` is a 🛑 short-circuiting action and the first match (OrderIndex = 1) wins. * Both `SetResponseHeader` actions will execute, resulting in `hello` and `hello2` headers being added. ## Edge Rule Execution Layers (Cache vs Origin) Edge Rules execute in two distinct stages of the request lifecycle: * Cache Layer - evaluated on every request, before cache lookup and cache decision are finalized * Proxy (Origin) Layer - evaluated only if the request goes to origin (CACHE MISS or BYPASS) > [!WARNING] > > The Cache Layer is always executed, regardless of whether the request results in a CACHE HIT or MISS. > > The Proxy (Origin) Layer is only executed when the request cannot be served from cache. ### Request Flow Overview A request is processed in the following order: 1. Edge Rules (Cache Layer) 2. Cache Lookup (HIT or MISS) 3. Edge Rules (Proxy/Origin Layer, only on MISS/BYPASS) 4. Origin Request (if needed) ### Cache Layer Execution (Always Runs): The following Edge Rules are evaluated at the Cache Layer, meaning they run on every request before cache resolution, regardless of whether the final result is a HIT or MISS. * `ForceSSL` (Enum Value: `0`) 🛑 * `Redirect` (Enum Value: `1`) 🛑 * `OverrideCacheTime` (Enum Value: `3`) 🛑 *Note: if set to`0` it will bypass edge cache, if we don't already have the content cached on our edge.* * `BlockRequest` (Enum Value: `4`) 🛑 * `SetResponseHeader` (Enum Value: `5`) 🔁 * `ForceDownload` (Enum Value: `7`) 🛑 * `DisableTokenAuthentication` (Enum Value: `8`) 🛑 * `EnableTokenAuthentication` ( Enum Value: `9`) 🛑 * `OverrideCacheTimePublic` (Enum Value: `10`) 🛑 * `IgnoreQueryString` (Enum Value: `11`) 🛑 * `DisableOptimizer` (Enum Value: `12`) 🛑 * `ForceCompression` (Enum Value: `13`) 🛑 * `SetStatusCode` (Enum Value: `14`) 🛑 * `OverrideBrowserCacheTime` (Enum Value: `16`) 🛑 * `SetNetworkRateLimit` (Enum Value: `18`) 🛑 * `SetConnectionLimit` (Enum Value: `19`) 🛑 * `SetRequestsPerSecondLimit` (Enum Value: `20`) 🛑 * `OverrideBrowserCacheResponseHeader` (Enum Value: `25`) 🛑 * `RemoveBrowserCacheResponseHeader` (Enum Value: `26`) 🛑 * `DisableShieldChallenge` (Enum Value: `27`) 🛑 ### Proxy (Origin) Layer Execution (MISS/BYPASS Only): The following Edge Rules are evaluated only when the request reaches the origin, meaning: * Cache MISS * Cache BYPASS (e.g. due to rules or headers) These rules are not executed on cache hits, as no origin request is made. * `OriginUrl` (Enum Value: `2`) 🛑 * `OverrideCacheTime` (Enum Value: `3`) 🛑 * `SetResponseHeader` (Enum Value: `5`) 🔁 * `SetRequestHeader` (Enum Value: `6`) 🔁 * `BypassPermaCache` (Enum Value: `15`) 🛑 * `OriginStorage` (Enum Value: `17`) 🛑 * `RunEdgeScript` (Enum Value: `21`) 🛑 * `OriginMagicContainers` (Enum Value: `22`) 🛑 * `DisableWAF` (Enum Value: `23`) 🛑 * `RetryOrigin` (Enum Value: `24`) 🛑 * `DisableShieldChallenge` (Enum Value: `27`) 🛑 * `DisableShieldBotDetection` (Enum Value: `29`) 🛑 * `BypassAwsS3Authentication` (Enum Value: `30`) 🛑 ## Setting the Execution Order ### Method 1: Dashboard (Drag & Drop) You can reorder your Edge Rules visually: 1. Go to your **Pull Zone** > **Edge Rules** tab. 2. Click "Reorder" badge at the top of the page. 3. Drag rules to change their order. 4. The order you set will automatically adjust the **OrderIndex** behind the scenes. ### Method 2: API (Set OrderIndex Directly) If you manage rules programmatically via our API, you can set the execution order by adjusting the `OrderIndex` value when creating or updating an edge rule. ## Best Practices * Plan rule priorities carefully, especially when multiple rules affect the same property or header. * Keep related rules grouped and ordered for easier maintenance. * Use the dashboard for visual management and the API for bulk or automated updates. # Pattern Matching in Edge Rules Source: https://bunny.net/docs/cdn/edge-rules/pattern-matching Use Lua-based pattern matching in Edge Rule conditions to match structured request values such as URLs, headers, cookies, and query strings. Edge Rules allow you to define logic that runs directly on bunny.net's edge servers before a request reaches your origin. Rules can modify caching behavior, redirect traffic, route requests to different origins, apply security actions, and more based on request conditions. Edge Rule conditions often evaluate values such as: * Cookie Value * Country Code (2 letter) * Country State Code * File Extension * Origin Retry Attempt Count * Query String * Random Chance (%) * Remote IP * Request Header * Request Method * Request URL * Response Header * Response Status Code In many cases, simple string or wildcard matching is sufficient. However, modern applications frequently generate URLs and request values that follow predictable patterns rather than fixed strings. Examples include: * Streaming manifests * Versioned build assets * Dynamically generated API routes To make these scenarios easier to handle, Edge Rules support **pattern matching using Lua patterns**. *** # Using Pattern Matching Pattern matching can be used in any Edge Rule condition that evaluates a request value. To enable pattern matching, prefix the value with: ```text theme={null} pattern:^...$ ``` Example: ```text theme={null} pattern:^.*/video_chunk%-[^%-]+%-[^%-]+%.dash$ ``` The `pattern:` prefix instructs the Edge Rule engine to evaluate the value using **Lua pattern matching** instead of a standard string comparison. Lua patterns are similar to regular expressions but intentionally simpler and optimized for performance. ## Anchoring the Pattern Patterns should normally be wrapped with: ```text theme={null} ^ $ ``` This ensures the **entire value is matched**, rather than matching a substring. If the `pattern:` prefix is not used, the condition behaves as a normal string comparison. *** # Example Pattern: ```text theme={null} pattern:^.*/video_chunk%-[^%-]+%-[^%-]+%.dash$ ``` Matches URLs such as: ```text theme={null} /live/video_chunk-us-12345.dash /live/video_chunk-es-54321.dash ``` Internally, Edge Rules evaluate patterns using Lua's `string.find` function. This provides efficient pattern matching without requiring a full regular expression engine. *** # Example Use Cases ## Matching Streaming Manifests Streaming platforms often generate manifest files containing region identifiers or session identifiers in the filename. Example URLs: ```text theme={null} /live/video_chunk-us-12345.dash /live/video_chunk-es-54321.dash ``` A pattern condition can match these files and apply actions such as: * Custom cache control * Edge redirects * Request routing *** ## Targeting Versioned Assets Many build systems generate versioned assets: ```text theme={null} /assets/app-1.4.7.js /assets/app-1.4.8.js /assets/app-1.5.0.js ``` A pattern condition allows matching all versions using a single rule. Common use cases include: * Cache rules * Rate limiting * Header modification *** ## Security Filtering Pattern matching can also be used in security rules. Example scenarios: * Blocking administrative endpoints * Detecting suspicious request patterns * Filtering specific URL structures These rules can be combined with conditions such as cookie presence or header values. *** # Lua Pattern Cheat Sheet Edge Rules use **Lua patterns**, which are simpler than full regular expressions. ### Format ```text theme={null} pattern:^...$ ``` Edge Rules evaluate patterns using Lua's `string.find`. ### Anchors | Symbol | Meaning | | - | - | | ^ | Start of string | | \$ | End of string | ### Character Classes | Pattern | Meaning | | - | - | | %d | Digit | | %a | Letter | | %w | Alphanumeric + \_ | | . | Any character | | \[abc] | Match a, b, or c | | \[^abc] | Match any character except a, b, or c | ### Repeaters | Pattern | Meaning | | - | - | | + | 1 or more | | \* | 0 or more (greedy) | | - | 0 or more (non-greedy) | ### Escaping Characters Use `%` to escape special characters. Examples: ```text theme={null} %. # literal dot %- # literal hyphen ``` ### Limitations Lua patterns are intentionally simpler than full regular expressions. Unsupported features include: * Alternation (`|`) * Lookaheads / lookbehinds * Full PCRE syntax These limitations help ensure pattern evaluation remains fast and predictable across the edge network. # Redirect b-cdn.net to Custom Hostname Source: https://bunny.net/docs/cdn/edge-rules/redirect-hostname Use Edge Rules to redirect your default b-cdn.net hostname to your custom CDN hostname. Edge Rules allow you to redirect your default `b-cdn.net` hostname to your custom hostname, ensuring your content is only accessible through your own domain. This is useful for: * SEO purposes (avoiding duplicate content) * Brand consistency * Ensuring all traffic goes through your custom domain ## Prerequisites * A pull zone with a [custom hostname](/docs/cdn/custom-hostname) already configured * The custom hostname should have SSL enabled ## Configuration **1. Open Edge Rules** Navigate to your pull zone and select **Edge Rules** from the side menu. **2. Add a new rule** Click **Add Edge Rule** to open the rule configuration page. **3. Configure the redirect action** Select **Redirect To URL** as the action. In the **Redirect URL** field, enter your custom URL with the `{{path}}` variable appended: ``` https://cdn.yourdomain.com{{path}} ``` > [!NOTE] > > The `{{path}}` variable automatically includes the full request path and query string, ensuring all URLs redirect correctly. Set the status code to **301** for a permanent redirect, or **302** for a temporary redirect. **4. Add a trigger condition** Click **Add Condition** and configure: | Setting | Value | | - | - | | **Condition Type** | Request URL | | **Match Type** | Match Any | | **Condition Value** | `*://yourzone.b-cdn.net/*` | Replace `yourzone` with the name of your pull zone. > [!WARNING] > > Make sure to include both the scheme wildcard (`*://`) and the trailing wildcard (`/*`) to match all requests to your b-cdn.net hostname. **5. Save the rule** Click **Save Edge Rule** to activate the redirect. ## Example configuration For a pull zone named `mycdn` with a custom hostname `cdn.example.com`: | Setting | Value | | - | - | | **Action** | Redirect To URL | | **Redirect URL** | `https://cdn.example.com{{path}}` | | **Status Code** | 301 | | **Condition Type** | Request URL | | **Condition Value** | `*://mycdn.b-cdn.net/*` | ### Before and after | Original URL | Redirected URL | | - | - | | `https://mycdn.b-cdn.net/images/logo.png` | `https://cdn.example.com/images/logo.png` | | `https://mycdn.b-cdn.net/video.mp4?quality=high` | `https://cdn.example.com/video.mp4?quality=high` | | `https://mycdn.b-cdn.net/` | `https://cdn.example.com/` | ## HTTP vs HTTPS Make sure to use the correct scheme in your redirect URL: * Use `https://` if your custom hostname has SSL enabled (recommended) * Use `http://` only if SSL is not configured Using the wrong scheme may cause redirect loops or connection errors. ## Related * [Custom Hostname](/docs/cdn/custom-hostname) - Set up a custom hostname for your pull zone * [SSL Setup](/docs/cdn/ssl-setup) - Configure SSL for your custom hostname * [Trigger Path Setup](/docs/cdn/edge-rules/trigger-path) - Learn more about configuring trigger conditions * [Dynamic Variables](/docs/cdn/edge-rules/dynamic-variables) - Full list of variables available in Edge Rules # Trigger Path Setup Source: https://bunny.net/docs/cdn/edge-rules/trigger-path Learn how to correctly configure Edge Rule trigger paths using wildcards and pattern matching. Edge Rules use a powerful wildcard matching system for trigger paths. Understanding how to properly configure these patterns helps you create precise rules that trigger only when needed. ## Wildcard basics The `*` wildcard matches any sequence of characters. You can use it anywhere in the trigger path: | Pattern | Matches | Does Not Match | | - | - | - | | `*.jpg` | `/image.jpg`, `/photos/sunset.jpg` | `/image.png`, `/image.jpg.bak` | | `/images/*` | `/images/logo.png`, `/images/photos/cat.jpg` | `/img/logo.png` | | `*://example.com/*` | `https://example.com/page`, `https://example.com/file.css` | `https://sub.example.com/page` | ## Common patterns ### Matching by file extension To match all files with a specific extension, use `*.extension`: ``` *.mp4 ``` This matches any `.mp4` file regardless of path depth. ### Matching subdomains Use wildcards to match all subdomains or exclude them: | Pattern | Description | | - | - | | `*://*.example.com/*` | Matches all subdomains | | `*://example.com/*` | Matches only the root domain (no subdomain) | | `*://*example.com/*` | Matches root domain and all subdomains | ### Matching specific paths | Pattern | Matches | | - | - | | `/api/*` | All paths starting with `/api/` | | `*/admin/*` | Any URL containing `/admin/` | | `/v1/*/users` | Paths like `/v1/api/users`, `/v1/service/users` | ## Important considerations ### Query parameters are not matched > [!WARNING] > > Trigger paths do not match query strings or any data after the filename. Only > the absolute URL path is evaluated. For example, if your trigger path is `*.jpg`, it will match: * `/image.jpg` * `/image.jpg?width=100` Both URLs match because the query string (`?width=100`) is ignored during pattern matching. ### HTTP vs HTTPS scheme matters The trigger path is compared as a complete string, including the scheme. Make sure to account for both protocols: | Pattern | Matches | | - | - | | `http://example.com/*` | Only HTTP requests | | `https://example.com/*` | Only HTTPS requests | | `*://example.com/*` | Both HTTP and HTTPS requests | > [!NOTE] > > Use `*://` to match both HTTP and HTTPS if your rule should apply regardless > of protocol. ### Trailing slashes are required for root paths Due to HTTP protocol design, the trigger path always contains a trailing slash when accessing a root path. > [!WARNING] > > A trigger path of `http://example.com` will **never** match because requests > to the root always include a trailing slash. Use `http://example.com/` > instead. | Trigger Path | Will Match Root? | | - | - | | `http://example.com` | No | | `http://example.com/` | Yes | | `*://example.com/*` | Yes | ## Examples > **Block access to admin paths** > > **Trigger path:** `*/admin/*` > > This blocks access to any URL containing `/admin/` in the path, such as: > > * `https://example.com/admin/dashboard` > * `https://example.com/app/admin/users` > **Match all image files** > > **Trigger path:** `*.jpg` (repeat for each extension) > > Or use the **File Extension** condition type instead, which allows you to specify extensions like `jpg`, `png`, `gif` without wildcards. > **Match a specific hostname** > > **Trigger path:** `*://cdn.example.com/*` > > This matches all requests to `cdn.example.com` regardless of protocol, path, or query string. > **Match paths starting with /api but not /api/public** > > You'll need two rules: > > 1. First rule (higher priority): Allow `/api/public/*` - use **Disable** action or skip > 2. Second rule (lower priority): Apply your action to `/api/*` > > See [Rule Ordering](/docs/cdn/edge-rules/ordering) for more on rule priority. ## Related * [Rule Ordering](/docs/cdn/edge-rules/ordering) - Control which rules execute first * [Dynamic Variables](/docs/cdn/edge-rules/dynamic-variables) - Use variables in your rule actions * [Custom Cache Time](/docs/cdn/edge-rules/custom-cache-time) - Example using file extension matching # Variable Expansion Source: https://bunny.net/docs/cdn/edge-rules/variable-expansion Use variable expansion syntax to dynamically configure Edge Rule values based on request parameters. Variable expansion allows Edge Rules to dynamically configure values based on specific request parameters. Instead of hardcoding URLs or header values, you can use variables that are replaced at runtime with actual request data. ## Supported actions Variable expansion works with the following Edge Rule actions: * **Change Origin URL** * **Redirect to URL** * **Set Request Header** * **Set Response Header** ## Syntax Variables are accessed using the following syntax: ``` %{Collection.Key} ``` Where `Collection` is a group of related variables, and `Key` is the specific value you want to access. > [!NOTE] > > For a complete list of all available variables with examples, see [Dynamic > Variables](/docs/cdn/edge-rules/dynamic-variables). *** ## Variable collections ### RequestHeaders The `RequestHeaders` collection contains HTTP headers from the incoming request. Headers are automatically mapped based on their name. | Example | Description | | - | - | | `%{RequestHeaders.Host}` | The `Host` header sent with the request | | `%{RequestHeaders.User-Agent}` | The `User-Agent` header | | `%{RequestHeaders.Accept-Language}` | The `Accept-Language` header | ### Query The `Query` collection contains URL query parameters. Parameters are automatically mapped based on their key. For a URL like `?token=abc123&user=42`: | Example | Result | | - | - | | `%{Query.token}` | `abc123` | | `%{Query.user}` | `42` | ### Request The `Request` collection contains information about the HTTP request: | Key | Description | Example | | - | - | - | | `Method` | The HTTP method | `GET`, `POST` | | `Path` | Full URL path and query | `/videos/test.mp4?quality=hd` | | `QueryString` | Query string only | `quality=hd` | ### Path The `Path` collection allows you to extract specific segments from the URL path using index-based access. For the path `/hello/world/bunny/eat/carrot.jpg`: | Syntax | Result | Description | | - | - | - | | `%{Path.0}` | `hello` | Single segment at index | | `%{Path.1}` | `world` | | | `%{Path.0-2}` | `hello/world/bunny` | Range from index 0 to 2 | | `%{Path.1-3}` | `world/bunny/eat` | Range from index 1 to 3 | | `%{Path.3-}` | `eat/carrot.jpg` | From index 3 to end | | `%{Path.1-}` | `world/bunny/eat/carrot.jpg` | From index 1 to end | | `%{Path.-1}` | `hello` | From start up to index 1 | | `%{Path.-3}` | `hello/world/bunny` | From start up to index 3 | ### Url The `Url` collection provides parsed components of the request URL: | Key | Description | Example | | - | - | - | | `FileName` | Filename from the URL (empty for folders) | `video.mp4` | | `Path` | Full URL path and query | `/videos/video.mp4?x=1` | | `Extension` | File extension | `mp4` | | `Directory` | Directory path without filename | `/videos/2024/` | | `Hostname` | Request hostname | `cdn.example.com` | ### User The `User` collection contains information about the requesting user: | Key | Description | Example | | - | - | - | | `IP` | End-user IP address | `203.0.113.10` | | `CountryCode` | Two-letter country code | `US`, `DE`, `JP` | ### Server The `Server` collection contains information about the serving edge server: | Key | Description | Example | | - | - | - | | `ZoneCode` | Zone code of the serving server | `NY`, `LA`, `DE` | | `ID` | Unique server ID | `9482` | *** ## Examples ### Dynamic origin routing Route requests to different origins based on the first path segment: **Origin URL:** `https://%{Path.0}.backend.example.com/%{Path.1-}` For a request to `/api/users/123`, this becomes `https://api.backend.example.com/users/123`. ### Pass query parameter as header Forward a specific query parameter to your origin as a header: **Header Name:** `X-Auth-Token`\ **Header Value:** `%{Query.token}` ### Geo-based routing Add the user's country to the origin request: **Header Name:** `X-Country`\ **Header Value:** `%{User.CountryCode}` *** ## Related * [Dynamic Variables](/docs/cdn/edge-rules/dynamic-variables) - Complete variable reference with all examples * [Rule Ordering](/docs/cdn/edge-rules/ordering) - Control execution order for complex rule sets * [Middleware Scripts](/docs/scripting/middleware/overview) - For advanced request/response transformations # Do you support cache warming or preloading content onto the CDN? Source: https://bunny.net/docs/cdn/frequently-asked-questions/cache-warming To ensure fair cache management and file distribution, we do not currently allow preloading files into cache. # Is there a domain limit on pull zone configurations? Source: https://bunny.net/docs/cdn/frequently-asked-questions/domain-limit Currently we have a limit of 10 hostnames per pull zone. If more are required, please reach out to [support@bunny.net](mailto:support@bunny.net). # Do you support ETag? Source: https://bunny.net/docs/cdn/frequently-asked-questions/etag-support ## What is an ETag? The `ETag` (Entity Tag) header is an HTTP response header used to uniquely identify a specific version of a file. It allows web browsers and CDNs to quickly determine whether a cached copy of a file is still identical to the one on the origin server. If a file changes, its ETag changes as well, making it a highly effective tool for validation, maximizing cache efficiency, and minimizing origin bandwidth usage. *** ## How Bunny.net Handles ETag Headers Bunny.net fully supports the `ETag` header out of the box, with a few important behavior considerations depending on your configuration: ### Origin Passthrough If your origin server includes an `ETag` header in its response and the **Optimize for large media delivery** setting is **disabled** in your Pull Zone, Bunny.net will pass the ETag through to the client and cache the resource accordingly. ### Smart Compression Adjustments When Bunny.net applies edge compression to a file (such as Brotli or Gzip), the original strong ETag will automatically be converted into a **weak ETag**. When this happens, an upstream `W/` prefix is added to the start of the header string: ```http theme={null} # Original Strong ETag from Origin ETag: "123456789" # Modified Weak ETag after Edge Compression ETag: W/"123456789" ``` # MIME Type Compression Support Source: https://bunny.net/docs/cdn/frequently-asked-questions/mime-compression Learn how BunnyCDN automatically compresses content using gzip, Brotli, and Zstandard, and view supported MIME types. BunnyCDN automatically compresses content using **gzip**, **Brotli** (`br`), or **Zstandard** (`zstd`), depending on the incoming `Accept-Encoding` header. Compression is guaranteed for a predefined set of MIME types listed below, as well as for files with the following extensions—even if the `Content-Type` header is missing or does not exactly match the list: * `.css` * `.js` * `.json` * `.xml` * `.svg` * `.html` BunnyCDN provides compression for these types, but it may also apply compression to other content types based on compatibility. > [!NOTE] > > If you don't see a specific MIME type listed, feel free to contact our support team, and we’ll be happy to review it for possible inclusion. ## Supported MIME Types | Type | MIME Content Type | | :- | :- | | **Application** | `application/atf` `application/atom+xml` `application/dash+xml` `application/ecmascript` `application/eot` `application/font` `application/font-sfnt` `application/javascript` `application/json` `application/ld+json` `application/manifest+json` `application/opentype` `application/otf` `application/pkcs7-mime` `application/rss+xml` `application/truetype` `application/ttf` `application/vnd.apple.mpegurl` `application/vnd.geo+json` `application/vnd.ms-fontobject` `application/wasm` `application/x-font-opentype` `application/x-font-truetype` `application/x-font-ttf` `application/x-httpd-cgi` `application/x-javascript` `application/x-mpegurl` `application/x-opentype` `application/x-otf` `application/x-perl` `application/x-ttf` `application/x-web-app-manifest+json` `application/xhtml+xml` `application/xml` `application/xml+rss` | | **Font** | `font/eot` `font/opentype` `font/otf` `font/truetype` `font/ttf` | | **Image** | `image/svg+xml` `image/vnd.microsoft.icon` `image/x-icon` | | **Model** | `model/gltf-binary` | | **Text** | `text/cache-manifest` `text/css` `text/csv` `text/html` `text/javascript` `text/js` `text/plain` `text/richtext` `text/tab-separated-values` `text/x-component` `text/x-java-source` `text/x-script` `text/xml` | # Which PoP you are being routed to Source: https://bunny.net/docs/cdn/frequently-asked-questions/pop-routing Learn how to identify which CDN edge server is handling your requests and how to check your routing details. BunnyCDN returns the server location and ID code with every request, which can help you easily determine which PoP and server you are being connected to. This guide explains how to get this information in three easy steps or by using automated diagnostic tools. ![Which PoP am I connected to](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/cdn/which-pop.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=97ee953e3e7f740eb8d7e189b66cad63) ### 1. Open your browser debug console First, open your web browser developer tools by pressing `Ctrl + Shift + I` (Windows/Linux) or `Cmd + Option + I` (Mac) on your keyboard, and switch to the **Network** tab in the newly opened window. *Note: In our example we are using Google Chrome, but the procedure is nearly identical across all modern web browsers.* ### 2. Find a bunny.net request in the network list Next, enter the hostname of your Pull Zone into your browser address bar (such as `https://mytestzone.b-cdn.net`) and navigate to it. After the page finishes loading, you will see a list of individual assets and network requests appear in the table. Select one of the requests going directly to BunnyCDN—this is typically the very first HTML document or initial request in the list. ### 3. Find the Server header in the request details After selecting the request, a detail panel will open displaying the headers returned by the edge node you connected to. Look closely under the **Response Headers** section for the `Server` header, which will look something like `BunnyCDN-{PoP Code}-{Server-ID}`. When debugging routing issues, you can use this to determine which location you are being routed to and if requested, forward this to our support team. ## Alternative option: using our Tools Report In addition to the above, you can use our new diagnostic tools here: [Bunny Diagnostic Tools](https://tools.bunny.net/diagnostic-report) which will show you some metrics about your connection: ![Diagnostic Example](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/diag-example.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=59ee821e69061b0e09c3ee83db7142fd) # Do you support range requests? Source: https://bunny.net/docs/cdn/frequently-asked-questions/range-requests BunnyCDN fully supports range requests for both cached and uncached content. By default, BunnyCDN enables range byte requests for any request involving cached content. Ensure that you activate 'Optimize for large object delivery' in your pullzone's caching settings to enable this for uncached content. # Do you support RTMP streaming? Source: https://bunny.net/docs/cdn/frequently-asked-questions/rtmp-streaming RTMP streams are currently not supported on our platform, but we are exploring what we can offer in the near future. # Does BunnyCDN automatically detect when a file is changed? Source: https://bunny.net/docs/cdn/frequently-asked-questions/updating-files BunnyCDN does not monitor the files on your origin server for changes, this means that if a file is already cached on our servers, it will remain cached until the Cache-Control expires or it gets deleted to make space for more popular content. If you make a change to a file and need to see it reflected immediately, please make sure to purge the cache using the purge tool available at [https://dash.bunny.net/purge](https://dash.bunny.net/purge) # Bunny CDN Source: https://bunny.net/docs/cdn/index Deliver content globally with smart caching, edge rules, and real-time analytics. ![bunny.net CDN](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/cdn.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=f33bd4bf80467ade000ef9e475b5af7e) Bunny CDN accelerates your websites, applications, and media by caching and delivering content from edge locations closest to your users. With servers spanning six continents, your content reaches audiences faster while reducing load on your origin server. > **[CDN Quickstart](/docs/cdn/quickstart)** ## Key features * **Global edge network**: Over 100 points of presence deliver content from the nearest location to each user * **Smart caching**: Intelligent cache management with configurable durations, query string handling, and vary headers * **Origin Shield**: Protect your origin server by consolidating requests through a single shield location * **Edge Rules**: Customize request handling, set headers, rewrite URLs, and implement redirects at the edge * **Token authentication**: Secure your content with signed URLs and time-limited access tokens * **WebSocket support**: Enable real-time bidirectional communication for interactive applications * **Perma-Cache**: Store content permanently at the edge for maximum cache hit rates * **Logging and analytics**: Access detailed logs via API, forward to storage, or enable permanent log retention ## How it works A Pull Zone connects your origin server to the Bunny CDN network. When a user requests content: 1. The request routes to the nearest edge location 2. If cached, the content serves immediately from the edge 3. If not cached, the edge fetches from your origin, caches the response, and delivers it This reduces round-trip time and offloads traffic from your infrastructure. # Discourse Source: https://bunny.net/docs/cdn/integrations/cms/discourse Speed up your Discourse forum with bunny.net CDN for faster page loads and improved performance. Discourse has built-in CDN support. This guide walks you through pointing it at Bunny CDN. **1. Create a Pull Zone** Log in to your [bunny.net dashboard](https://dash.bunny.net) and open the **Add Pull Zone** page. Choose a name for your zone, then set the origin URL to your Discourse forum (for example, `discourse.example.com`). ![Creating a Pull Zone for Discourse](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/discourse-add-zone.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=c575489d17162c3de7f5c2ad18fb67d3) Select a pricing tier and click **Add Pull Zone**. For more detail, see [How to create your first Pull Zone](/docs/cdn/quickstart). **2. Configure Discourse with your Pull Zone** Open your `app.yml` configuration file and find the following lines: ```yaml theme={null} ## the origin pull CDN address for this Discourse instance DISCOURSE_CDN_URL: https://discourse-cdn.example.com ``` If they aren't present, add them below the other `DISCOURSE_` variables. Replace the `DISCOURSE_CDN_URL` value with your Pull Zone hostname. ![Setting DISCOURSE\_CDN\_URL in app.yml](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/discourse-cdn-url.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=762ddfe32e7aece9bad185198cf655af) **3. Rebuild the Discourse container** Apply the new configuration by rebuilding the container: ```bash theme={null} ./launcher rebuild app ``` Once the rebuild finishes, your forum is served through Bunny CDN. To confirm everything is working, see [Verify your configuration](/docs/cdn/verify-configuration). # Drupal Source: https://bunny.net/docs/cdn/integrations/cms/drupal Integrate bunny.net CDN with your Drupal website for faster content delivery and improved performance. Drupal supports CDN delivery through the [CDN module](https://www.drupal.org/project/cdn), which rewrites your asset URLs to serve them through Bunny CDN. This guide walks you through installing and configuring it. **1. Create a Pull Zone** Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone with your Drupal site as the origin URL. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Match the protocol of your site exactly. If your Drupal site runs on HTTPS, enable HTTPS on the Pull Zone; if it runs on HTTP only, leave HTTPS disabled. A mismatch will cause errors. > [!NOTE] > > You can set a long Cache-Control time for Drupal. The CDN module serves updated elements from new URLs, so changes appear immediately without waiting for the cache to expire. **2. Install the CDN module** Download the latest `.tar.gz` release of the [Drupal CDN module](https://www.drupal.org/project/cdn) and copy its download URL. You'll need SSH or FTP access to your site. In the Drupal admin, select **Manage**, then **Extend**, then **Install new module**. ![Drupal Extend page with the Install new module button](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/drupal-extend-install-module.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=8cf48833da5f4cc67b7f03b751f3d836) Paste the `.tar.gz` URL, click **Install**, and provide your SSH/FTP credentials when prompted. When the installer finishes, click **Enable newly added modules**. Scroll to the **Web Services** section, select the **CDN** and **CDN UI** modules, then click **Install**. ![Enabling the CDN and CDN UI modules](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/drupal-enable-cdn-modules.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=d3ed182a2a5fb0d29edb5408f5a48d35) **3. Configure the CDN module** Go to **Manage → Configuration**, scroll to the bottom, and click **CDN Integration**. On the **Status** tab, make sure **Serve files from a CDN** is enabled. ![Enabling Serve files from a CDN](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/drupal-cdn-status.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=9ae9915c9ec68a301aed91c1d4043427) Open the **Mapping** tab, select **Simple** mapping, choose **Serve all files**, and enter your Pull Zone hostname in the field on the right. Click **Save configuration**. ![Configuring the CDN mapping](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/drupal-cdn-mapping.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=bd7b168ce37db2fe0f36dd908d263be5) Your Drupal site now serves static assets through Bunny CDN. # ExpressionEngine Source: https://bunny.net/docs/cdn/integrations/cms/expressionengine Configure bunny.net CDN with ExpressionEngine to accelerate your website and optimize asset delivery. Setting up Bunny CDN with ExpressionEngine takes only a single line of configuration plus a template change. **1. Create a Pull Zone** Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone with your ExpressionEngine site as the origin URL. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). **2. Edit your index.php file** You'll need SSH or FTP access to your web server. Open the `index.php` file in your site's root and find this line: ```php theme={null} // $assign_to_config['global_vars'] = array(); ``` Uncomment it and define a global variable for your CDN URL, replacing the hostname with your Pull Zone: ```php theme={null} $assign_to_config['global_vars'] = array('cdn_url' => 'https://YOURZONE.b-cdn.net/'); ``` **3. Use the variable in your templates** Update your templates to reference the new `{cdn_url}` variable when linking to static resources. For example: ```text theme={null} {cdn_url}uploads/image.jpg ``` **4. Test the integration** Load your site and confirm that the URLs for your static content now include your Pull Zone hostname. Once they do, ExpressionEngine is serving assets through Bunny CDN. # Joomla Source: https://bunny.net/docs/cdn/integrations/cms/joomla Speed up your Joomla website with bunny.net CDN using the CDN for Joomla plugin. Joomla can serve assets through Bunny CDN using the [CDN for Joomla plugin](https://www.regularlabs.com/extensions/cdnforjoomla) by Regular Labs, which rewrites your content URLs to serve them through the CDN. > [!NOTE] > > The free version of the CDN for Joomla plugin only supports HTTP URLs. To use HTTPS URLs, you'll need the paid version. **1. Install the CDN for Joomla plugin** Download the plugin from the [Regular Labs site](https://www.regularlabs.com/extensions/cdnforjoomla). In your Joomla admin, go to **Extensions → Manage → Install**, then upload the ZIP file you downloaded. ![Installing the CDN for Joomla plugin](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/joomla-install-plugin.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=6171968e21f99111c0285d6007c6392d) **2. Open the plugin configuration** Go to **Extensions → Manage → Manage**, use the search to find the CDN plugin, then click it to open its configuration page and switch to the **Setup** tab. ![Finding the CDN plugin](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/joomla-find-plugin.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=55bb30109b98326a961d2c99b2715b4a) **3. Create a Pull Zone** In your [bunny.net dashboard](https://dash.bunny.net), create a new Pull Zone with a name, set the origin URL to your website, choose your tier, and click **Add Pull Zone**. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). ![Creating a Pull Zone](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/joomla-create-pull-zone.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=7b194abcb01c07e79470e102d8281874) **4. Set the CDN Domain** Back on the plugin's **Setup** tab, enter your Pull Zone hostname (the `b-cdn.net` hostname) in the **CDN Domain** field, then click **Save**. ![Setting the CDN Domain](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/joomla-cdn-domain.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=511624b8be0a61fc66d7a59f68840dcf) Your Joomla site now serves content through Bunny CDN. # Magento Source: https://bunny.net/docs/cdn/integrations/cms/magento Integrate bunny.net CDN with your Magento store for faster page loads and optimized e-commerce performance. This guide walks you through serving your Magento store's static and media files through Bunny CDN in four steps. **1. Create a Pull Zone** Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone with your Magento store as the origin URL. Match the protocol (HTTP or HTTPS) of your site exactly. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). ![Creating a Pull Zone for Magento](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/magento-add-pull-zone.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=ff29792d0bdfe17135eccc7c998c55e4) **2. Add the required CORS headers** Open your Pull Zone's **Headers** settings and add the `html` and `json` extensions. This is required, otherwise your Magento admin interface won't work correctly. ![Adding CORS headers to the Pull Zone](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/magento-cors-headers.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=e366e8ea24143eaa2f797d6d940703bd) **3. Configure Magento to use the Pull Zone** Log in to your Magento admin panel and go to **Stores → Configuration → General → Web**. ![Magento Web configuration](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/magento-web-config.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=14590e95aacb988709c71f5edbe1b90f) Open the **Base URLs** tab and enter your Pull Zone hostname in both **Base URL for Static View Files** and **Base URL for User Media Files**, including the `/static/` and `/media/` subfolders respectively. ![Setting the Base URLs](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/magento-base-urls.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=de5941d8d3ffff8bd5e099d2530a0bed) Repeat the same values in the **Base URLs (Secure)** section, using the `https://` version of the URL to avoid connectivity issues. Click **Save Config**. **4. Flush the Magento cache** Flush your Magento cache as prompted by the settings dashboard. Your Magento store is now serving static and media files through Bunny CDN. # PrestaShop Source: https://bunny.net/docs/cdn/integrations/cms/prestashop Speed up your PrestaShop store with bunny.net CDN for faster product pages and improved checkout performance. PrestaShop includes built-in CDN settings, so connecting it to Bunny CDN is straightforward. **1. Create a Pull Zone** Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone with your PrestaShop store as the origin URL. Match the protocol (HTTP or HTTPS) of your site exactly. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). **2. Open the Performance panel** Log in to your PrestaShop admin panel and go to **Advanced Parameters → Performance**. ![PrestaShop Advanced Parameters menu](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/prestashop-performance-menu.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=1577db04dc197b67baec0cbb17a3eab3) **3. Configure the CCC section** In the **CCC (Combine, Compress and Cache)** section, set **Smart cache for CSS** and **Smart cache for JavaScript** to **Yes**. ![Configuring the CCC section](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/prestashop-ccc.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=a74f09508012dc5ff51e0433a46c08ab) > [!NOTE] > > On older versions of PrestaShop, this section shows checkboxes instead of dropdowns. In that case, select **Use CCC for CSS** and **Use CCC for JavaScript**. Click **Save**. **4. Configure the Media Servers** Scroll down to the **Media Servers** section and enter your Pull Zone hostname in the **Media server #1** field. Click **Save**. ![Configuring Media server #1](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/prestashop-media-server.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=2bfafdbbc794b728d6b8871d9b5892c1) **5. Clear the cache (optional)** If caching is enabled for your store, click **Clear cache** at the top of the page. Your pages will now be served through Bunny CDN. # Shopware Source: https://bunny.net/docs/cdn/integrations/cms/shopware Configure bunny.net CDN with Shopware to accelerate your e-commerce store and improve customer experience. Shopware can serve media through Bunny CDN using a community-built adapter that uploads your content to a Bunny Storage zone and delivers it through a connected Pull Zone. > [!NOTE] > > The [Shopware adapter](https://github.com/tinect/TinectMediaBunnycdn) is built and maintained by the community, not by bunny.net. **1. Create a Storage Zone and Pull Zone** The adapter uploads your media to a Bunny [Storage Zone](/docs/storage), which is then delivered through a Pull Zone that uses the Storage Zone as its origin. Set up both before configuring Shopware. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). **2. Install the Shopware adapter** Download the latest `TinectMediaBunnycdn.zip` from the adapter's [GitHub releases page](https://github.com/tinect/TinectMediaBunnycdn/releases). In Shopware, open **Configuration → Plugin Manager**. ![Opening the Shopware Plugin Manager](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/shopware-plugin-manager.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=3e20989f957fd67fdbe4535da2865f64) Go to **Installed**, click **Upload Plugin**, and select the ZIP file you downloaded. ![Uploading the plugin ZIP file](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/shopware-upload-plugin.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=afa22ab539389c049f2f1c0c36be8ba1) The plugin appears under **Uninstalled**. Click the green **+** to install it. ![Installing the uploaded plugin](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/shopware-enable-plugin.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=77c8c710de249e117afdcf05865a9a6d) Once installed, the plugin is **Inactive**. Click the pencil icon next to its name, then click **Activate**. If prompted to clear caches, confirm. ![Activating the plugin](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/shopware-activate-plugin.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=c3e0ba5882f87dbf37f87f425a268b86) **3. Configure Shopware to use Bunny CDN** You'll need SSH or FTP access to edit a PHP file. In your Shopware install directory, open `config.php` and append the following to the configuration array: ```php theme={null} 'cdn' => [ 'backend' => 'bunnycdn', 'adapters' => [ 'bunnycdn' => [ 'type' => 'bunnycdn', 'mediaUrl' => 'https://PULLZONE.b-cdn.net/', 'apiUrl' => 'https://storage.bunnycdn.com/STORAGEZONENAME/', 'apiKey' => 'secret-api-key', ], ], ], ``` Replace `PULLZONE` with your Pull Zone hostname, `STORAGEZONENAME` with your Storage Zone name, and `apiKey` with the password from your Storage Zone's **FTP & API Access** page. Finally, upload your existing local media to the Storage Zone: ```bash theme={null} bin/console sw:media:migrate --from=local --to=bunnycdn ``` Your Shopware store now serves media through Bunny CDN. # Typo3 Source: https://bunny.net/docs/cdn/integrations/cms/typo3 Integrate bunny.net CDN with your TYPO3 website for faster content delivery and improved performance. TYPO3 can serve assets through Bunny CDN using the [Content Replacer extension](https://extensions.typo3.org/extension/replacer/), which rewrites the content URLs on your site. This guide walks you through the setup. **1. Create a Pull Zone** Log in to your [bunny.net dashboard](https://dash.bunny.net) and create a new Pull Zone with your TYPO3 site as the origin URL. For details, see [How to create your first Pull Zone](/docs/cdn/quickstart). Match the protocol of your site exactly. If your TYPO3 site runs on HTTPS, enable HTTPS on the Pull Zone; if it runs on HTTP only, leave HTTPS disabled. A mismatch will cause errors. **2. Install the Content Replacer extension** If your TYPO3 install uses Composer mode, install the extension from the command line: ```bash theme={null} composer req jweiland/replacer ``` ![Installing the Content Replacer extension with Composer](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/typo3-composer-install.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=74274cc75ee3f04857dab50aaadcae8a) Otherwise, install it through the **Extension Manager** in the TYPO3 web interface. **3. Configure the Content Replacer** Go to **Template**, select the root page, enable **Modify**, and edit the entire template record. ![Editing the TYPO3 template record](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/typo3-edit-template.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=8b8a9ca9a0747b769353f798a119c0cd) Scroll to the **Setup** box and paste the following, replacing `YOURZONE.b-cdn.net` with your Pull Zone hostname: ```typoscript theme={null} config.tx_ja_replacer { search { 1 = typo3temp/ 2 = fileadmin/ 3 = typo3conf/ } replace { 1 = https://YOURZONE.b-cdn.net/typo3temp/ 2 = https://YOURZONE.b-cdn.net/fileadmin/ 3 = https://YOURZONE.b-cdn.net/typo3conf/ } } ``` Click **Save**, then select the lightning bolt and click **Flush all caches**. ![Flushing all caches in TYPO3](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/cdn/integrations/cms/typo3-flush-caches.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=f93bc19242b83ebdfdec2257227329ec) Your TYPO3 site now serves static assets through Bunny CDN. # WordPress Source: https://bunny.net/docs/cdn/integrations/cms/wordpress Speed up your WordPress site with bunny.net CDN using our official plugin for easy integration and optimization. # Serve Astro from Bunny CDN with Bunny Optimizer Source: https://bunny.net/docs/cdn/integrations/frameworks/astro Put a bunny.net pull zone in front of a self-hosted Astro app, and resize astro:assets images at the edge with Bunny Optimizer. A custom image service sends every `` width to [Bunny Optimizer](/docs/optimizer/dynamic-images/overview), which resizes and converts it at the edge. Your server never resizes an image, and the pull zone caches each width. The [example app](https://github.com/BunnyWay/examples/tree/main/cdn/optimizer-astro-standalone) covers both setups on this page, and runs Astro with the [Node adapter](https://docs.astro.build/en/guides/integrations-guide/node/). > [!NOTE] > > Astro apps deployed to Magic Containers with a CDN endpoint already come with a pull zone in front of the whole app. Turn on Optimizer for that zone with the steps in [Add Optimizer](/docs/magic-containers/guides/astro#add-optimizer). For a static Astro site on Bunny Storage, see [Resize images with Bunny Optimizer](/docs/storage/static-site-hosting/astro#resize-images-with-bunny-optimizer). ## Set up Optimizer and the image service **1. Create a pull zone** [Create a pull zone](/docs/cdn/quickstart) with your app's origin URL, then turn on [URL Query String](/docs/cdn/vary-cache#url-query-string) vary. Without it, `?width=1080` comes back as the cached `?width=640` image. **2. Enable Optimizer** Enable [Bunny Optimizer](/docs/optimizer/quickstart) on the pull zone, then turn on the Dynamic Image API and automatic WebP under **Optimizer → Settings**. Astro already minifies CSS and JavaScript, so turn Optimizer's minification off. **3. Add the image service** The service spreads Astro's `baseService`, so Astro still works out the `srcset` widths, and only `getURL` changes. It adds `?width=` and `?quality=` to each image on your own zone, and leaves images on other hosts alone, since Optimizer can't reach them. ```ts src/bunny-image-service.ts theme={null} import type { ExternalImageService } from "astro"; import { baseService } from "astro/assets"; const service: ExternalImageService<{ baseURL?: string }> = { ...baseService, getURL(options, imageConfig) { const base = imageConfig.service.config?.baseURL; const src = typeof options.src === "string" ? options.src : options.src.src; const absolute = /^(https?:)?\/\//.test(src); if (absolute && !(base && src.startsWith(base))) return src; const url = new URL(src, base || "http://localhost"); if (options.width) url.searchParams.set("width", String(options.width)); url.searchParams.set("quality", String(options.quality ?? 75)); return base || absolute ? url.href : url.pathname + url.search; }, }; export default service; ``` **4. Configure Astro** ```js astro.config.mjs theme={null} import { defineConfig } from "astro/config"; import node from "@astrojs/node"; // Assets and images only: your pull zone URL const cdn = process.env.CDN_URL; export default defineConfig({ output: "server", adapter: node({ mode: "standalone" }), build: { assetsPrefix: cdn }, image: { service: { entrypoint: "./src/bunny-image-service.ts", config: { baseURL: cdn } }, }, }); ``` Use `` with a `layout`, and Astro writes the `srcset`: ```astro theme={null} --- import { Image } from "astro:assets"; import photo from "../assets/photo.jpg"; --- Photo ``` Images in `src/assets` get a hashed `/_astro` URL, which the Node adapter serves with a one-year `immutable` header. Images in `public` are served with `max-age=0`, so the pull zone revalidates them with your origin on every cache miss. ## Choose what the pull zone serves **Assets and images only** Visitors load HTML from your host, and `build.assetsPrefix` points `/_astro` files and images at the pull zone. **1. Respect origin cache headers** Set the pull zone to respect the origin's `Cache-Control` header, so the `/_astro` files stay cached for a year. **2. Block every other path** Add an [edge rule](/docs/cdn/edge-rules) with the **Block Request** action that triggers when the request URL matches none of `*/_astro/*` and `*/images/*`. Without it, the pull zone serves your pages on its own hostname. **3. Build with the pull zone URL** Astro reads `CDN_URL` when it builds: ```bash theme={null} CDN_URL=https://myapp.b-cdn.net npm run build ``` Deploy `dist` and start it with `node ./dist/server/entry.mjs`, for example on [Magic Containers](/docs/magic-containers/guides/astro). The image `srcset` URLs in the page source should start with your pull zone hostname. If you self-host fonts, they load from the pull zone hostname too, so add `woff2` and `woff` under the pull zone's **Headers** for CORS. **Whole site** Your public hostname points at the pull zone, and every request goes through it. **1. Add your hostname** Add your public hostname to the pull zone as a [custom hostname](/docs/cdn/custom-hostname), and point its DNS at the pull zone. **2. Keep cookies** Turn off **Disable Cookies** in the pull zone's caching settings. Pull zones created through the API strip `Set-Cookie` by default, so a login looks like it worked and the session is gone after a reload. **3. Keep Smart Cache on** Astro sends no `Cache-Control` header on a server-rendered page. With [Smart Cache](/docs/cdn/smart-cache) off, the pull zone caches those pages for its default expiry and can hand one visitor's page to the next. Smart Cache keeps HTML out of the edge cache, so deploys need no purge either. Form posts and Astro Actions need no config. Astro's CSRF check reads the browser's `Sec-Fetch-Site` header, which the pull zone passes through unchanged. Build without `CDN_URL`, then submit a form that sets a cookie and reload the page to check it stuck. > [!NOTE] > > `CDN_URL` must be set when `astro build` runs. If you build in Docker, declare it as an `ARG` in the build stage and pass it with `--build-arg`. ## Check it works Point these at your pull zone or public hostname: ```bash theme={null} # Run twice, and look for CDN-Cache: HIT on the second request curl -sI https://myapp.b-cdn.net/_astro/.js | grep -i cdn-cache # Expect image/webp curl -sI -H 'Accept: image/webp' 'https://myapp.b-cdn.net/images/photo.jpg?width=640&quality=75' | grep -i content-type ``` # Serve Next.js from Bunny CDN with Bunny Optimizer Source: https://bunny.net/docs/cdn/integrations/frameworks/nextjs Put a bunny.net pull zone in front of a self-hosted Next.js app, and resize next/image requests at the edge with Bunny Optimizer. A custom `next/image` loader sends every image request to [Bunny Optimizer](/docs/optimizer/dynamic-images/overview), which resizes and converts it at the edge. Your origin never runs `/_next/image`, and the pull zone caches each width. The [example app](https://github.com/BunnyWay/examples/tree/main/cdn/optimizer-nextjs-standalone) covers both setups on this page. > [!NOTE] > > Next.js apps deployed to Magic Containers with a CDN endpoint already come with a pull zone in front of the whole app. Turn on Optimizer for that zone with the steps in [Add Optimizer](/docs/magic-containers/guides/nextjs#add-optimizer). For a static Next.js site on Bunny Storage, see [Resize images with Bunny Optimizer](/docs/storage/static-site-hosting/nextjs#resize-images-with-bunny-optimizer). ## Set up Optimizer and the loader **1. Create a pull zone** [Create a pull zone](/docs/cdn/quickstart) with your app's origin URL, then turn on [URL Query String](/docs/cdn/vary-cache#url-query-string) vary. Without it, `?width=1080` comes back as the cached `?width=640` image. **2. Enable Optimizer** Enable [Bunny Optimizer](/docs/optimizer/quickstart) on the pull zone, then turn on the Dynamic Image API and automatic WebP under **Optimizer → Settings**. Next.js already minifies CSS and JavaScript, so turn Optimizer's minification off. **3. Add the loader** The loader turns each `next/image` width into an Optimizer URL. It needs `"use client"` because the App Router sends the loader to the browser. ```typescript lib/bunny-loader.ts theme={null} "use client"; import type { ImageLoaderProps } from "next/image"; const cdn = process.env.NEXT_PUBLIC_CDN_URL; export default function bunnyLoader({ src, width, quality }: ImageLoaderProps) { const url = new URL(src, cdn || "http://localhost"); const params = url.searchParams; if (!params.has("width")) params.set("width", String(width)); if (!params.has("quality")) params.set("quality", String(quality ?? 75)); const absolute = cdn || /^https?:\/\//.test(src); return absolute ? url.href : url.pathname + url.search; } ``` Any parameters already on `src` stay, so [Dynamic Image API](/docs/optimizer/dynamic-images/overview) options combine with the responsive widths: ```tsx theme={null} Photo ``` Optimizer defaults to quality 85. The loader sends Next.js's default of 75 unless a component sets its own. **4. Configure Next.js** ```typescript next.config.ts theme={null} import type { NextConfig } from "next"; // Assets and images only: your pull zone URL const cdn = process.env.NEXT_PUBLIC_CDN_URL; // Whole site: your public hostnames, comma separated const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(","); const nextConfig: NextConfig = { output: "standalone", assetPrefix: cdn, experimental: { serverActions: { allowedOrigins } }, images: { loader: "custom", loaderFile: "./lib/bunny-loader.ts", qualities: [50, 75], }, async headers() { return [ { source: "/images/:path*", headers: [{ key: "Cache-Control", value: "public, max-age=86400" }], }, ]; }, }; export default nextConfig; ``` Next.js 16 only accepts quality 75 unless you list other values in `images.qualities`. The `headers()` rule caches `public/images` for a day, since Next.js serves public files with `max-age=0`. ## Choose what the pull zone serves **Assets and images only** Visitors load HTML from your host, and Next.js loads scripts, styles, fonts, and images from the pull zone. **1. Respect origin cache headers** Set the pull zone to respect the origin's `Cache-Control` header. Next.js sends year-long immutable headers on `/_next/static/*`. **2. Add CORS headers for fonts** Add `woff2` and `woff` under **Headers**. `next/font` serves fonts from `/_next/static/media` on the pull zone hostname. **3. Block every other path** Add an [edge rule](/docs/cdn/edge-rules) with the **Block Request** action that triggers when the request URL matches none of `*/_next/static/*` and `*/images/*`. Without it, the pull zone serves your pages on its own hostname, and Next.js sends `s-maxage=31536000` on static pages, so the edge can cache a year-old copy of your HTML. **4. Build with the pull zone URL** Next.js inlines `NEXT_PUBLIC_` variables at build time: ```bash theme={null} NEXT_PUBLIC_CDN_URL=https://myapp.b-cdn.net npm run build ``` Deploy the standalone build, for example to [Magic Containers](/docs/magic-containers/guides/nextjs). The ` ``` Once your application has finished fetching data and rendering the final DOM, set it to `true`: ```javascript theme={null} window.prerenderReady = true; ``` HTML Prerender will then capture the page once rendering is complete. \[!NOTE] A maximum rendering timeout is enforced. If a page takes too long to finish rendering, HTML Prerender may still capture its current state before all content is available. ## Limitations * Only publicly accessible URLs are prerendered * Pages requiring authentication are not prerendered * Complex client-side interactions may not be captured # Image Classes Source: https://bunny.net/docs/optimizer/image-classes Define reusable transformation presets Image Classes let you create named presets for Dynamic Images API transformations, making it easier to maintain consistent image styles across your website. ## Why use Image Classes? Instead of this: ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?width=300&height=200&quality=85&sharpen=true ``` Use this: ```bash theme={null} https://yourzone.b-cdn.net/image.jpg?class=thumbnail ``` **Benefits:** * Consistent transformations across your site * Easier to update styles (change the class, not every URL) * Improved cache hit rates * Enhanced security with forced transformation classes ## Creating Image Classes **1. Navigate to Image Classes** Navigate to **Optimizer** → **Image Classes** ![Image Classes overview](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/optimizer/image-classes/image-classes-overview.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=dcea4cec9705eb5e8d4fe938f52a7082) **2. Define your class properties** Configure the transformation settings for your image class: * Width, height, aspect ratio * Quality settings * Filters and effects * Any Dynamic Images API parameter ![Add image class properties](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/optimizer/image-classes/image-classes-add-property.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=d5478ba65e0d3ad68e3e5017bd2fbab2) **3. Save the class** Save the class with a memorable name that describes its purpose (e.g., `thumbnail`, `hero`, `product-mobile`). ## Forced transformation classes Enable this option to restrict the Dynamic Images API to only use predefined classes: * Improves cache efficiency (fewer unique transformations) * Prevents arbitrary transformations (security benefit) * Returns 403 for invalid or missing classes This is recommended for production environments where you want tight control over image transformations. ## Example classes | Property | `thumbnail` | `hero_image` | `mobile_product` | | - | - | - | - | | Width | 300px | 1920px | 800px | | Height | 200px | - | - | | Aspect Ratio | - | 16:9 | - | | Quality | 80% | 90% | 75% | | Additional Settings | Sharpen: true | - | Smart optimization: enabled | # Bunny Optimizer Source: https://bunny.net/docs/optimizer/index Bunny Optimizer accelerates your website with automatic asset optimization, smart routing, and dynamic image manipulation. ![bunny.net Optimizer](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/optimizer/optimizer.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=702943c14099f8d70ae1b19253694969) Bunny Optimizer works alongside [Bunny CDN](/docs/cdn) to make your website faster through automatic optimization and intelligent delivery. > **[Optimizer Quickstart](/docs/optimizer/quickstart)** ## What's included Users typically see significant improvements: * Up to **5x faster load times** with WebP compression * Up to **40% faster uncached requests** with Burrow * Up to **80% file size reduction** for images Fast websites improve user experience and increase conversion rates. Optimizer helps you deliver that performance with just a few clicks. > **[Automatic Optimization](/docs/optimizer/automatic-optimization)** > > WebP compression, CSS/JS minification, responsive image optimization, and > watermarking with unlimited requests. > **[Dynamic Images API](/docs/optimizer/dynamic-images/overview)** > > Real-time image manipulation via URL parameters. Resize, crop, transform, and > apply filters on the fly. > **[Burrow Smart Routing](/docs/optimizer/burrow-smart-routing)** > > Intelligent path selection for uncached requests with up to 40% faster > response times. > **[HTML Prerender](/docs/optimizer/html-prerender)** > > Fully rendered HTML for search engines and social crawlers. Improves SEO for > SPAs with no code changes. > **[Smart Preloader](/docs/cdn/performance/smart-preloader)** > > Display a branded loading screen when your origin is slow to respond, > improving perceived performance. > **[Agent Readability](/docs/optimizer/agent-readability)** > > Agent Readability makes your website easier and more efficient for AI systems to consume. # Optimizer limits Source: https://bunny.net/docs/optimizer/limits Overview of the default limits for the Bunny Optimiser The following limits are fixed and cannot be changed. *** | Limit | Value | | - | - | | Maximum time (in seconds) to download an image from the origin | 20 | | Maximum time (in seconds) spent processing an image | 60 | | Maximum upscaling size (in megapixels) | 24 | | Maximum AVIF output area (in megapixels) | 4 | # Pricing Source: https://bunny.net/docs/optimizer/pricing Simple, predictable pricing for optimising your website. Bunny Optimizer is priced at \$9.50 per Pull Zone per month and includes unlimited optimisation, requests, and transformations. CDN bandwidth and Burrow usage are billed separately. For details on Burrow usage pricing, see [Burrow pricing](/docs/optimizer/burrow-smart-routing#burrow-pricing). # Quickstart Source: https://bunny.net/docs/optimizer/quickstart Enable Bunny Optimizer and start optimizing your website in minutes **1. Access the CDN Zone** 1. From your Bunny dashboard, go to the left-hand navigation menu 2. Select the Pull Zone where you want to enable Bunny Optimizer 3. Once inside your Pull Zone, scroll down in the left-hand menu and click on “Optimizer” ![Access the CDN Zone](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/optimizer/locate-optimizer.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=691480d6eb6219063a254e98a6d167ce) **2. Enable Bunny Optimizer** You will now see the Bunny Optimizer overview screen Click the orange button labeled “Turn on Bunny Optimizer.” That’s it! Bunny Optimizer is now active for your zone ![Access the CDN Zone](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/optimizer/enable-bunny-optimizer.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=363de767da225777f26216d5ac042676) That's it! Your CSS, JavaScript, and images are now being automatically optimized. **3. Configure automatic optimization (optional)** **Image optimization:** * Set maximum image widths for desktop and mobile * Adjust quality settings (0-100%) * Enable smart device-specific optimization **Minification:** * Toggle CSS minification * Toggle JavaScript minification **WebP compression:** * Automatically serves WebP to supported browsers * No URL changes required See the [Automatic Optimization](/docs/optimizer/automatic-optimization) guide for detailed configuration options. **4. Try the Dynamic Images API (optional)** Transform images on the fly by adding query parameters: 1. Go to **Optimizer** → **Settings** 2. Enable **Dynamic Image API** Now you can manipulate any image: ```bash theme={null} # Original https://yourzone.b-cdn.net/photo.jpg # Resize to 500px width https://yourzone.b-cdn.net/photo.jpg?width=500 # Create 16:9 thumbnail https://yourzone.b-cdn.net/photo.jpg?aspect_ratio=16:9&width=256 ``` Learn more in the [Dynamic Images guide](/docs/optimizer/dynamic-images). **5. Enable advanced features** > **[Burrow Smart Routing](/docs/optimizer/burrow-smart-routing)** > > Routes uncached requests over optimized paths for faster origin response > times. > **[HTML Prerender](/docs/optimizer/html-prerender)** > > Improves SEO for single-page applications by serving rendered HTML to > crawlers. # Troubleshooting Source: https://bunny.net/docs/optimizer/troubleshooting This page provides guidance on how to troubleshoot issues with the Bunny Optimiser The information on this page should be used for troubleshooting purposes only. Clients should not rely on these headers or error codes, as they may change in the future. ## Response headers Responses from the Bunny Optimiser may include the following headers: * `X-DownloadSize`: Size of the original asset downloaded from the origin * `X-BO-CompressionRatio`: Percentage reduction in size of the final asset compared to the original * `X-BO-OriginDownloadTime`: Time spent downloading files from the origin (including the watermark, if present), in milliseconds * `X-BO-ProcessingTime`: Time spent on local processing, in milliseconds * `X-BO-Processing-Error`: Lists any errors that occurred while processing the request (see below) ### Error codes The following errors may be returned as the value of the `X-BO-Processing-Error` header. #### General errors * `101` → An unknown error occurred * `102` → The original source file could not be downloaded from the origin * `103` → The source image could not be decoded or parsed as a valid image * `104` → Optimisation was skipped or failed because the output became larger than the original * `105` → The GIF file is invalid or cannot be processed * `106` → Text-based content could not be minified successfully * `107` → The requested output format is not supported #### Watermark-related * `200` → The watermark image could not be retrieved from its origin * `201` → The watermark origin request was cancelled before completion * `202` → The watermark image could not be decoded or parsed as a valid image * `204` → The watermark image URL returned a 404 Not Found response #### Processing constraints * `301` → The generated or requested WebP image exceeds the allowed size limit * `302` → The requested resize would upscale the image, but upscaling is not enabled * `303` → The requested upscale exceeds the allowed limit * `304` → The AVIF output image exceeds the allowed size limit and was encoded in another format instead # Image Watermarking Source: https://bunny.net/docs/optimizer/watermarking Automatically protect and brand your images with custom watermarks Bunny Optimizer can automatically apply watermarks to your images to protect intellectual property, reinforce branding, or mark user-generated content. > [!NOTE] > > Watermarks are applied during the optimization process and cached. There's no > performance penalty after the first request. ## Configuration Access watermarking settings in **Optimizer** → **Settings** → **Watermark images** ![Enable Watermarking](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/optimizer/image-watermarking.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=fd8638d0562c626c61b022e8e7ceed0c) ### Watermark image URL The publicly accessible URL to your watermark PNG (transparency supported). **Best practices:** * Use PNG format with transparency * Design at 2x resolution for sharp display * Keep file size under 100KB * Test on both light and dark images ### Minimum image size Only images with width OR height greater than this value will be watermarked (in pixels). **Example:** Setting this to 800px prevents watermarks on small thumbnails while protecting larger downloads. ### Position Choose from a few different placement options: * Bottom Right * Bottom Left * Top Right * Top Left * Center * Center Stretched ### Border offset Distance from the edge as a percentage (0-100%). **Example:** 5% offset on a bottom-right watermark places it 5% away from both the bottom and right edges. ## Combining with Dynamic Images API Watermarking applies after Dynamic Images transformations, ensuring your watermark appears on all generated variants. # bunny.net Partner Portal Source: https://bunny.net/docs/partners/index Manage your customers, their pricing, products, and billing as a bunny.net partner. ![bunny.net Partner Portal](https://mintcdn.com/bunnynet-cb9733c2/EZ8jfRbW09QxdtsA/images/partners/bunny-partners.png?fit=max\&auto=format\&n=EZ8jfRbW09QxdtsA\&q=85\&s=07d97b5376356f15835808a7b3e2b5b2) The Partner Portal is where bunny.net partners manage the accounts they resell to. Add customers, set the pricing they pay, control which products they can use, and review their billing and account activity. > [!NOTE] > > The Partner Portal is available to partners only. [Contact us](https://bunny.net/contact/) if you would like to join the partner program. ## What you can manage | Area | Description | | - | - | | Customers | Create new customer accounts, or invite existing bunny.net users to become customers | | Pricing | Review your partner pricing, and the effective prices and overrides for each customer | | Products | Enable or disable individual products for each customer | | Contract | Review the contract details for your partner account | | Billing | View a customer's billing summary and invoice history, and download invoice PDFs | | Audit Log | Review the actions taken on a customer's account, by date | ## Add a customer There are two ways to bring a customer under your partner account: * **Create a customer.** bunny.net creates the account and emails the customer a link to set their password. * **Invite a customer.** An existing bunny.net user receives an invitation, and is assigned to you once they accept. Both are available in the dashboard and through the API. ## Automate with the API Every part of the portal is also available over HTTP, so you can sync customers with your own systems, pull billing data into your invoicing, or provision accounts at signup. > **[Partner Portal API](/docs/api-reference/partner)** > > Manage customers, pricing, products, and billing programmatically # Product Release Stages Source: https://bunny.net/docs/product-release-stages We introduce new products and major features through a phased release process. This allows us to validate real-world and production use cases, ensure reliability and performance, gather customer feedback, and align support and go-to-market readiness before declaring a product fully production-ready. Each stage sets clear expectations around access, pricing, stability, and support. ## **Release stages at a glance** | Stage | Access | Pricing | Intended use | | - | - | - | - | | **Closed Preview** | Invite-only | Free | Early validation with trusted users | | **Public Preview** | Public | Free or paid (varies by product) | Real-world and **limited production** workloads | | **General Availability (GA)** | Public | Paid | Production workloads | ## **Stage 1: Closed Preview** **Closed Preview** is an invite-only early access phase focused on validating core functionality with a small group of trusted users. During this stage, we work closely with participants to validate use cases and iterate rapidly. ### **Access and pricing** * Access: Invite-only * Pricing: Free ### **What you can expect** * Functional end-to-end, but not fully polished * A **“Preview”** label in the UI and documentation * Features and APIs may change * Feedback directly influences the roadmap * Users are selected from early adopters, partners, or the community ### **Support and reliability** * Best-effort support * Not recommended for mission-critical production workloads ## **Stage 2: Public Preview** **Public Preview** makes the product available to a broader audience for real-world validation at scale. Some customers may also choose to run production workloads during this phase, depending on their risk tolerance and use case. ### **Access and pricing** * Access: Public * Pricing: **May be free or paid**, depending on the product and underlying infrastructure or operational costs Pricing during Public Preview varies by product and is always clearly communicated before use. ### **What you can expect** * A visible **“Preview”** label * Stable and reliable for real-world usage * Suitable for **production workloads**, with the understanding that: * Features may still evolve * Breaking changes are unlikely but possible * Onboarding, documentation, and self-serve flows are available * Some features may still be under active development ### **Support and reliability** * Support is available through standard or community channels * Feedback continues to drive prioritization and improvements *** ## **Stage 3: General Availability (GA)** **General Availability (GA)** represents the full public launch of a product. At this stage, the product is production-ready and fully supported for long-term use. ### **Access and pricing** * Access: Public * Pricing: Paid (standard pricing applies) ### **What you can expect** * A stable, production-grade product * Fully finalized documentation and APIs * Mature onboarding, monitoring, and operational tooling * Designed for broad adoption and long-term scalability ### **Support and status** * Support is handled through established customer support channels * Service health and incidents are communicated via the status page when applicable *** ## **Release labeling** We use the following labels to clearly communicate product maturity: * **Preview**: Applies to Closed Preview and Public Preview releases * **GA**: Indicates the product is production-ready with full support # Get started with bunny.net Source: https://bunny.net/docs/quickstart It's free to get started. ### Create an account > [!NOTE] > > Your free trial includes **\$20 in trial credits** with no payment card > required. Add billing information to unlock an additional **\$30 in trial > credits** for a total of \$50. Trial credits expire at the end of your trial > and any unused balance is removed. **1. Sign up for your free trial** [Sign-up](https://dash.bunny.net/auth/register) and register for a free trial account. ![Sign up page](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/77d6443f3afc17417d9a584536a804efac8082c2cd21d9796bc1eaec842d6bdc-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=5d5638aaa77b695cbd73b4e62ea238c5) **2. Confirm your email address** You must use the link in the email sent to you. Once done, you'll be able to create and manage as many resources in your account as you need during the trial. ![Email confirmation prompt](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/3f950cf7f1c403c27a55a4b5d368ae87f7163d5d037117c7177e4cf5f4dcbd5a-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=b6be60792986dea5fca49723ac888598) **3. Claim extra credit (optional)** After confirming your email, you can claim an additional **\$30 in credits** by adding your billing information to your account. ![Claim trial credits](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/f760efbeb8d3eea705460bdaef5f379e53fbd5356b625a139ac4b1c9d395ef39-image.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=26d5c70811fd41d101ff9e6054205af3) This step is optional but highly recommended, as it provides even more value during your trial period. To add billing information, click on **Claim \$30 trial credits** and fill in the required information: ![Billing information form](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/5fd977d097c4ed39542e9aeea7c31f19b6dc3241588b27aa616c6f11ace88da3-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=e22ec829808a6d3fc2bc3ce5e038b28b) > [!NOTE] > > With a total of \$50 in credits, you'll be able to explore all the powerful features of the bunny.net platform. **4. Start building** Your dashboard includes a handy countdown timer that tracks the remaining days of your trial. This feature helps you plan your usage effectively, ensuring you make the most of your trial period. ![Trial countdown timer](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/94d768ad14d9e83eb39ccc20b9005cf88cd5bad724b1fc878ac53b5769f19d9c-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=ef9aa7fe1ddc555853f19b1f25cc3474) ### Trial balance notifications We will send you email alerts when your trial balance reaches key thresholds: * **Unverified accounts**: Notifications at \$10 and \$1 remaining * **Verified accounts** (with payment card): Notifications at \$20 and \$5 remaining All services are charged at our standard rates and deducted from your trial balance. If you add funds to your account, charges will continue to be deducted from your trial balance first until it is exhausted or expired. ### When your trial expires Once your 14-day trial expires, any remaining trial credits are removed from your account. Trial credits cannot be carried over or converted to paid credits. ![Trial expired](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/b3731853229c9b6abbc4a6aeb32633832987fa473f2b79bb63eabe64687766be-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=22eb328f6a28887da2bb60dfb156d8cc) However, you can continue enjoying bunny.net's services by clicking the **Recharge Your Account** button and adding funds. **Upgrade** ### Upgrading to a paid account You can add funds to your account anytime during or after the trial. Even after adding funds, you will continue to use your trial credits until they expire, ensuring a seamless transition. When you top up your account, the full amount you add will be credited to your balance, regardless of your negative trial balance. To reactivate your account, simply add funds to bring your balance positive. **Refer & Earn** ### Refer your friends and earn credits The bunny.net platform also offers an exciting **refer-a-friend program** that rewards you for sharing the platform with others. ![Referral program](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/725cc9a067a6b14fe574ff00bcadd2b82d08e3394a7cfcce24bef9a7b326390c-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=edc8473708d75b9b59f581e3d9ffec20) For every friend you refer who signs up and becomes a paying customer, you'll earn **\$20 in credits**. There's no limit to how many friends you can refer—more referrals mean more credits for you! To participate, share your **unique referral link** (available in your dashboard) with friends, colleagues, or anyone who could benefit from bunny.net's services. # API Reference Source: https://bunny.net/docs/scripting/api-reference Complete API reference for deploying and managing Edge Scripts programmatically. # Before cache execution Source: https://bunny.net/docs/scripting/before-cache Run your edge scripts before the cache layer to take control of every request, instead of only executing when the cache is missed. By default, edge scripts run **after** the cache. The CDN serves cached responses directly, without invoking your script, so cached traffic is not slowed down by script execution. With before cache execution enabled, your script runs on **every request**, before the cache lookup. This gives you full control over the request flow at the cost of executing your script even for cached content. Post-cache execution remains the default. Enable before cache execution when you need per-request logic, such as authentication, tenant-aware routing, or custom caching rules. > [!WARNING] > > Overriding the default handling of `/.well-known/*` paths in your script can > break ACME challenges and prevent automated certificate renewal. ## Enable before cache execution Before cache execution is enabled at the Pull Zone level and applies to the script linked to that Pull Zone. Navigate to your Pull Zone, then go to **General** > **Origin** and enable **Run script before cache**. For standalone scripts, you can use the same Pull Zone toggle or enable it directly in the script settings. ## Middleware scripts When before cache execution is enabled, middleware scripts gain access to two additional hooks that run on the client side of the cache: * **`onClientRequest`**: runs on every incoming request before the cache lookup. Modify the request or short-circuit by returning a response directly. * **`onClientResponse`**: runs just before the response is sent to the client, including responses served from the cache. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk@0.13.0"; BunnySDK.net.http .servePullZone({ url: "https://your-origin.com" }) .onClientRequest((ctx) => { // Runs before the cache lookup on every request return Promise.resolve(ctx.request); }) .onClientResponse((ctx) => { // Runs before the response is sent to the client return Promise.resolve(ctx.response); }); ``` See [Middleware scripts](/docs/scripting/middleware/overview) for the full hook reference and workflow. ## Standalone scripts Standalone scripts act as the origin for their Pull Zone, so by default they only execute when the cache is missed. With before cache execution enabled, your script handles **every request** before the cache lookup, and decides whether and how responses are cached. See [Standalone scripts](/docs/scripting/standalone/overview) for details. ## Workflow ```mermaid theme={null} sequenceDiagram participant Client participant Script participant Cache participant Origin Client->>Script: Request Script->>Cache: Cache lookup Cache->>Origin: On cache MISS Origin-->>Cache: Response Cache-->>Script: Response Script-->>Client: Response ``` # Cache examples Source: https://bunny.net/docs/scripting/cache/examples End-to-end Cache API recipes for Edge Scripting. ## Quickstart This example caches an HTML page at the edge, then rewrites part of it per request from a query parameter. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { const url = new URL(request.url); const now = new Date(); const expires = new Date(now.getTime() + 60 * 1000); // + 1min try { const cache = await caches.open("cache:v1"); // 1. check to see if we already have a cache entry let res = await cache.match(`${url.origin}/example.html`); // 2. if we don't, create one and put it into the cache for 60 seconds if (!res) { res = new Response( `

Hello

`, { headers: { "Cache-Control": "max-age=60", "Content-Type": "text/html; charset=utf-8", }, }, ); await cache.put(`${url.origin}/example.html`, res.clone()); } // 3. modify the contents const content = new HTMLRewriter() .on("span", { element(el) { el.setInnerContent( url.searchParams.get("name") ?? "Nameless User (use ?name= to define this)", ); }, }) .transform(res); // 3. return modified contents to the user return new Response(await content.text(), { headers: { "Cache-Control": "no-cache", "Content-Type": "text/html; charset=utf-8", }, }); } catch (error) { return new Response(`Error: ${error.message}`, { status: 500, }); } }, ); ``` ## Updating a cache in middleware This example writes to the cache from middleware when the incoming request is a POST, and reads the entry back from a standalone script. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; async function onOriginRequest(context: { request: Request; }): Promise | Response | Promise | Request | void { const now = new Date(); try { const cache = await caches.open("cache:v1"); if (context.request.method == "POST") { const res = new Response(`Content from POST: ${now.toISOString()}`, { headers: { "Cache-Control": "max-age=3600", "Content-Type": "text/plain; charset=utf-8", }, }); Bunny.v1.waitUntil(cache.put(context.request, res)); } } catch (e) { console.log(e); } } BunnySDK.net.http.servePullZone().onOriginRequest(onOriginRequest); ``` ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { try { const cache = await caches.open("cache:v1"); let res = await cache.match(request); if (!res) { const res = Response(await "Content from GET", { headers: { "Cache-Control": "no-cache", "Content-Type": "text/plain; charset=utf-8", }, }); } return res; } catch (error) { return new Response(`Error: ${error.message}`, { status: 500, }); } }, ); ``` ## Refresh and purge a cached value on demand Expose a single URL that serves a cached JSON payload on `GET`, regenerates it on `POST`, and clears it on `DELETE`. Useful for cache-warming endpoints, manual invalidation hooks, or scheduled refreshes from an upstream job. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk@0.13.0"; BunnySDK.net.http.serve(async (request: Request): Promise => { const url = new URL(request.url); const cache = caches.default; // Share one key across all three methods by normalizing the request to a // GET on the same URL. const cacheKey = new Request(url.toString(), { method: "GET" }); try { if (request.method === "POST") { const fresh = Response.json( { generatedAt: new Date().toISOString(), random: Math.random(), }, { headers: { "Cache-Control": "s-maxage=60" }, }, ); await cache.put(cacheKey, fresh.clone()); return new Response(`Cached "${cacheKey.url}" for 60s`, { headers: { "Cache-Control": "no-cache" }, }); } if (request.method === "GET") { const hit = await cache.match(cacheKey); if (hit) { return hit; } return new Response( "No cached value yet. POST to this URL to generate one.", { status: 404, headers: { "Cache-Control": "no-cache" } }, ); } if (request.method === "DELETE") { const deleted = await cache.delete(cacheKey); return new Response( deleted ? `Purged "${cacheKey.url}"` : `Nothing to purge for "${cacheKey.url}"`, { status: deleted ? 200 : 404, headers: { "Cache-Control": "no-cache" }, }, ); } return new Response("Method Not Allowed", { status: 405, headers: { "Allow": "GET, POST, DELETE", "Cache-Control": "no-cache", }, }); } catch (e) { return new Response(`Cache error: ${(e as Error).message}`, { status: 500 }); } }); ``` > [!NOTE] > > The POST handler awaits `cache.put()` so the acknowledgement reflects that > the value actually landed. For fire-and-forget refreshes, wrap it with > `Bunny.v1.waitUntil(cache.put(cacheKey, fresh.clone()))` and return > the ack immediately. ## References * [Cache API overview](./index): regional behavior, limitations, and API surface. * [Managing caches](./managing-caches): the global `caches` object and named cache instances. * [Reading & writing](./reading-writing): the `match`, `put`, and `delete` method reference. * [Runtime: waitUntil](../runtime#waituntil): extend the isolate lifetime for background cache writes. # Cache API Source: https://bunny.net/docs/scripting/cache/index Fine-grained control over reading and writing from the bunny.net edge cache from Edge Scripting. The Cache API is an implementation of the [MSDN Cache Interface](https://developer.mozilla.org/en-US/docs/Web/API/Cache). It stores Request and Response pairs in long lived memory. The API is available globally, but cache contents do not replicate outside the originating region. A GET `/users` response cached in one region will not exist in another until a request for that resource is made from there. An origin can have multiple, named Cache objects, and it is up to your script to decide how cache updates happen. Items in a Cache respect the `Cache-Control` header on the response you pass to `put()`, which is how you control the life cycle of your caches. Entries are purged automatically a short time after they expire. Version your caches by name, and only use a cache from a version of the script that can safely operate on it. Cache instances are shared across all domains associated with your PullZone, but they **must** be accessed via the currently requesting host name. Use the current `Request` object as the key, or build the key from the current request URL, for example `const key = new URL(req.url).origin + "/img/example.png";`. Either way your caches will work reliably across every domain on the PullZone. > [!NOTE] > > **Note:** There is a hard limit of `100MB` per cache file. ## Limitations API surface limits: * **`CacheStorage`** (the global `caches` object) * Supported: `caches.default`, `caches.open(name)` * Not supported: `caches.has`, `caches.delete`, `caches.keys`, `caches.match` * **`Cache`** (an instance returned by `caches.default` or `caches.open`) * Supported: `match`, `put`, `delete` * Not supported: `matchAll`, `add`, `addAll`, `keys` We recommend versioning your cache names (e.g. `cache:v1`, `cache:v2`) so that a cache is completely purged when updating scripts. ## Pull Zone cache settings The Cache API is not independent of the Pull Zone. The **Cache expiration time** setting of the connected Pull Zone also applies to entries your script writes. If **Cache expiration time** is set to **Override: do not cache**, `cache.put()` resolves without an error, but it stores no entry. A later `cache.match()` for the same key returns `undefined`. > [!WARNING] > > Do not set **Override: do not cache** to keep the CDN from caching the responses your script returns. This also stops the Cache API from storing entries. To cache entries in your script, but not the responses it returns: 1. In the Pull Zone, click **Caching** and set **Cache expiration time** to **Respect origin Cache-Control**. 2. Set a `Cache-Control` header with a TTL, such as `max-age=60`, on each response you pass to `cache.put()`. 3. Set `Cache-Control: no-cache` on the responses your script returns to the client. The [Examples](./examples) page uses this pattern. ## Quickstart A minimal cache-aside pattern: look up by URL, generate on miss, write back in the background. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk@0.13.0"; BunnySDK.net.http.serve(async (request: Request): Promise => { const url = new URL(request.url); try { // Normalize the key to a GET on the request URL so reads and writes // resolve to the same entry regardless of the inbound method. const cacheKey = new Request(url.toString(), { method: "GET" }); const cache = caches.default; const cached = await cache.match(cacheKey); if (cached) { console.log(`Cache hit for: ${request.url}.`); return cached; } console.log(`Cache miss for: ${request.url}. Generating and caching.`); // In a real script this is typically `await fetch(originUrl)`. const response = Response.json( { value: Math.random() }, { headers: { "Cache-Control": "s-maxage=10" } }, ); // Fire-and-forget the write so we can return immediately. Bunny.v1.waitUntil(cache.put(cacheKey, response.clone())); return response; } catch (e) { return new Response(`Cache error: ${(e as Error).message}`, { status: 500 }); } }); ``` See [Examples](./examples) for longer recipes: HTMLRewriter integration, middleware writes, and on-demand refresh and purge. ## Troubleshooting > **cache.put() succeeds, but cache.match() returns undefined** > > 1. Check the **Cache expiration time** setting of the connected Pull Zone. If it is **Override: do not cache**, set it to **Respect origin Cache-Control**. See [Pull Zone cache settings](#pull-zone-cache-settings). > 2. Make sure the response you store has a `Cache-Control` header with a TTL, such as `max-age=60`. > 3. Use the same key for `put()` and `match()`. Build the key from the hostname of the current request, and normalize the method to `GET`. > 4. Test the write and the read in the same request, and `await` the `put()`. Cache contents do not replicate between regions, so two separate requests can reach different regions. > > This diagnostic script writes an entry and reads it back in one request: > > ```typescript theme={null} > import * as BunnySDK from "@bunny.net/edgescript-sdk"; > > BunnySDK.net.http.serve(async (request: Request): Promise => { > const cache = await caches.open("cache:diagnostic"); > const key = new Request(new URL("/cache-test", request.url).toString(), { > method: "GET", > }); > > await cache.put( > key, > new Response("ok", { headers: { "Cache-Control": "max-age=60" } }), > ); > const hit = await cache.match(key); > > return new Response(hit ? "HIT" : "MISS", { > headers: { "Cache-Control": "no-cache" }, > }); > }); > ``` > > If this returns `MISS`, the problem is in the Pull Zone configuration, not in your script. ## References * [MSDN CacheStorage](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage) - Mozilla's CacheStorage interface documentation * [MSDN Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache) - Mozilla's Cache interface documentation # Managing caches Source: https://bunny.net/docs/scripting/cache/managing-caches The global caches object, covering the default cache and how to open named caches. ## Default cache The `caches.default` API follows the web browsers' Cache API interface closely. It gives you a single, unnamed cache shared across requests. ```typescript theme={null} await caches.default.match(request); ``` ## Named caches You may create and manage additional Cache instances via the `caches.open` method. Named caches are useful when you want to version cache contents (e.g. `cache:v1`, `cache:v2`) so a deploy can switch over cleanly. ```typescript theme={null} const cache = await caches.open("cache:v1"); await cache.match(request); ``` > [!NOTE] > > **Note:** When using the Cache API, avoid overriding the hostname in cache requests, as this can lead to unexpected behaviour when using multiple domains associated with a PullZone. > > ```typescript theme={null} > // recommended approach: use current request url > const url = new URL(request.url); > > const cache = await caches.open("cache:v1"); > const response = await cache.match(`${url.origin}/example.html`); > ``` ## References * [Reading & writing](./reading-writing): the `match`, `put`, and `delete` method reference. * [Examples](./examples): end-to-end recipes using named and default caches. * [Cache API overview](./index): regional behavior, limitations, and API surface. # Reading & writing Source: https://bunny.net/docs/scripting/cache/reading-writing Reading from, writing to, and deleting entries on a Cache instance. ## Cache-Control behavior Cache entry lifetimes are controlled via the `Cache-Control` header on the response you pass to `put()`. The same directives that govern HTTP caching apply here. For the full directive list, see [Cache-Control on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control). ```typescript theme={null} await cache.put( `${url.origin}/example.html`, new Response("content", { headers: { "Cache-Control": "max-age=60" }, }), ); ``` This caches the string `content` under the key `/example.html` for 60 seconds. ## Instance methods ### Delete The `delete()` method finds the Cache entry whose key matches the request and removes it, returning `true` if an entry was found and deleted, or `false` if there was no matching entry. #### Syntax ```typescript theme={null} await cache.delete(request); ``` Unlike the browser Cache API, Bunny Edge Scripting does not support the options bag (`ignoreSearch`, `ignoreMethod`, `ignoreVary`). To get equivalent behaviour, normalize the key at `put()` time, for example by stripping query strings from the URL before calling `put()`. #### Parameters `request` The Request you are looking to delete. This can be a Request object or a URL. #### Return Value A `Promise` that resolves to `true` if a matching entry was found and removed, or `false` if no entry existed. After the promise resolves, a subsequent `cache.match()` for the same key will miss. #### Exceptions Can throw an exception if there is an issue accessing the underlying cache storage. #### Example ```typescript theme={null} const cache = await caches.open("cache:v1"); const deleted = await cache.delete(`${url.origin}/example.html`); ``` See [Refresh and purge a cached value on demand](./examples#refresh-and-purge-a-cached-value-on-demand) for a full request handler that exposes `DELETE` as a purge endpoint. ### Match The `match()` method of the Cache returns a Promise that resolves to the Response associated with the first matching request in the Cache object. If no match is found, the Promise resolves to `undefined`. #### Syntax ```typescript theme={null} await cache.match(request); ``` Unlike the browser Cache API, Bunny Edge Scripting does not support the options bag (`ignoreSearch`, `ignoreMethod`, `ignoreVary`). You can accomplish equivalent behaviour by removing query strings or unwanted headers from the key at `put()` time. #### Parameters `request` The Request for which you are attempting to find responses in the Cache. This can be a Request object or a URL. #### Return Value A `Promise` that resolves to the first `Response` that matches the request or to `undefined` if no match is found. #### Exceptions Can throw an exception if there is an issue accessing the underlying cache storage. #### Example ```typescript theme={null} const cache = await caches.open("cache:v1"); const hit = await cache.match(`${url.origin}/example.html`); if (hit) return hit; ``` See [Quickstart](./examples#quickstart) for a full handler that pairs a `match()` lookup with an `HTMLRewriter` transform. ### Put The `put()` method of the Cache allows key/value pairs to be added to the current Cache object. #### Syntax ```typescript theme={null} await cache.put(request, response); ``` #### Parameters `request` The Request object or URL that you want to add to the cache. `response` The Response you want to match up to the request. #### Return Value A `Promise` that resolves with `undefined`. The promise can resolve without storing an entry. This happens when the connected Pull Zone has **Cache expiration time** set to **Override: do not cache**. See [Pull Zone cache settings](./index#pull-zone-cache-settings). #### Exceptions Can throw an exception if there is an issue accessing the underlying cache storage. #### Example ```typescript theme={null} const cache = await caches.open("cache:v1"); // clone() so the response body can still be returned to the caller. await cache.put(`${url.origin}/example.html`, response.clone()); ``` See [Updating a cache in middleware](./examples#updating-a-cache-in-middleware) for a full handler that populates the cache in response to origin requests. ## References * [Examples](./examples): end-to-end recipes for cache-aside, middleware writes, and refresh and purge. * [Managing caches](./managing-caches): the global `caches` object and named cache instances. * [Cache API overview](./index): regional behavior, limitations, and API surface. # Deployments Source: https://bunny.net/docs/scripting/deployments Track the published versions of your script and roll back to an earlier one when you need to. A deployment is a published version of your script. Every time you publish, a new deployment is created, so you keep a record of what changed and can go back to an earlier version if a release causes problems. Your team can see the same history. ## Accessing the deployments view To view your deployments: 1. Log in to the [bunny.net dashboard](https://dash.bunny.net/auth/login?pk_buttonlocation=menu). 2. Select **Edge Platform**, click on **Scripting**, and select the script. ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/10b1ccfc6b8c02a117ad495c2444a3491e7405843d5f809ad2c10c96be6af67d-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=2265befb11aed1fdef6ef9c355bee014) 3. Open the **Deployments** tab and select **Latest**. ![](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/de450432ca801cdf3d195c1afd89a781b37bd3877b22ae9aa93ca4060a4e06aa-image.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=364166630dea51a7fd99114decfecce3) The current deployment is listed alongside every previous one. ## Publishing previous deployments To revert to an earlier version: 1. Open the **Deployments** tab and select **Latest**. ![](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/fd8af2222acd93066de5120c598f1e6582209c19e949e8fbc40105c791a9d48e-image.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=8196f5cf53c7b2f3806e6971f93a3d81) 2. Click **Publish** next to the version you want to deploy. ![](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/e788e81577a931585cb9db58890571351257738bafdb963c76d647279549ae06-image.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=b07e57c5806acbfbec7c06c1381c437d) # Environment Variables Source: https://bunny.net/docs/scripting/environment-variables Environment variables are used to store information or configuration data that your script requires at runtime. Environment variables hold configuration data that your script reads at runtime. Because the values live outside the code, they never end up committed to your script or your version control system. > [!NOTE] > > For sensitive data such as API keys, passwords, or tokens, use [environment > secrets](/docs/scripting/secrets) instead. Secrets are encrypted and cannot be > viewed once set. ## Adding environment variables To add a variable to your script: 1. Log in to the [bunny.net dashboard](https://dash.bunny.net/auth/login?pk_buttonlocation=menu). 2. Select **Edge Platform**, click on **Scripting**, and select the script. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/b2cf84a71bbd1c6af9b87f320055e681a16782c609a7b192d5279319b79f843c-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=a4533f1732dafb5e82c062b3917b30ce) 3. Select **Env Configuration** and **Environment Variables**, and fill in the details of the variable you want to create. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/6890047328f8c5e9d652f9405138062ff8e04f515d719b8cd8d8cccdc9ad0db2-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=8336332a89eaf77fb785d8129fe346f1) 4. Click **Save**. ## Using environment variables Read the value in your script with either the Node.js `process` module or `Deno.env`. This example uses the `process` module: ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; import process from "node:process"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { // Load environment variable named OriginUrl const originUrl = process.env.OriginUrl; // Parse initial url const url = new URL(request.url); // Rewrite and fetch request from origin return fetch(originUrl + url.pathname); }, ); ``` This one does the same thing with `Deno.env`: ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { // Load environment variable named OriginUrl const originUrl = Deno.env.get("OriginUrl"); // Parse initial url const url = new URL(request.url); // Rewrite and fetch request from origin return fetch(originUrl + url.pathname); }, ); ``` # Deploying your scripts with GitHub Actions Source: https://bunny.net/docs/scripting/github-actions Use the bunny.net deploy action to publish an Edge Script from a GitHub repository whenever your code changes. The `BunnyWay/actions/deploy-script` action uploads a script to Edge Scripting as part of a GitHub workflow, so a push to the branch you choose publishes the script without anyone having to upload it by hand. ## Example usage This workflow deploys a script whenever something is pushed to the main branch: ```yml theme={null} name: Deploy when pushing on main on: push: branches: - "main" jobs: publish: runs-on: ubuntu-latest name: "Upload script" steps: - name: Checkout repository uses: actions/checkout@v4 - name: Deploy Script to Bunny Edge Scripting uses: BunnyWay/actions/deploy-script@main with: script_id: ${{ secrets.SCRIPT_ID }} deploy_key: ${{ secrets.DEPLOY_KEY }} file: "script.ts" ``` ## Inputs The action takes three inputs: * `script_id` (required): the ID of the script you want to deploy. Store it as a GitHub secret (`SCRIPT_ID`). * `deploy_key` (required): the key that authorizes the deployment. Store it as a GitHub secret (`DEPLOY_KEY`). * `file` (required): the path of the script file to deploy. The file has to exist in your repository, or an earlier step in the workflow has to generate it. The script ID and deploy key are available in the dashboard under **Script** -> **Deployments** -> **Settings**. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/c72efa4529d3fef3f23326fb60487c15a77e88928abc317bf8e6c719acf5a8e4-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=215f2eeaf30b83a01366c263bd57f1ab) Place the workflow file in the `.github/workflows` folder in your repository, and adjust it to fit your own build and deployment pipeline. The [official GitHub documentation](https://docs.github.com/en/actions/writing-workflows) covers workflow syntax in full. ## Setting up secrets To store the `script_id` and `deploy_key` as secrets in your GitHub repository: 1. Navigate to your GitHub repository. 2. Click on **Settings**. 3. Select **Secrets and variables**. 4. Choose **Actions**. 5. Click **New repository secret**. 6. Add `SCRIPT_ID` and `DEPLOY_KEY` as the names of the secrets and provide their respective values. # GitHub Integration Source: https://bunny.net/docs/scripting/github-integration Keep your Edge Script code in a GitHub repository and deploy it with GitHub Actions. The GitHub integration lets you keep your Edge Script code in a GitHub repository instead of the dashboard editor. You get version control, and you can run the script through GitHub Actions before it ships: builds, tests, minification, security scanning, or anything else your pipeline needs. This suits larger applications, where the script is built from multiple files or prebuilt resources rather than written in a single editor tab. ## What you'll need * A [bunny.net](https://bunny.net/) account ([log in](https://dash.bunny.net/auth/login?pk_buttonlocation=menu) or sign up for a [free trial](https://dash.bunny.net/auth/register)). * A GitHub account. ## Creating a new repository Start here if the project is new and you want to base it on a bunny.net template. 1. Login to the bunny.net dashboard. 2. Navigate to **Edge Platform** > **Scripting** and click **Add Script**. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/67f0ac06eb60d9f2e7bcfe49f7fe689a3bd45155a4fcb431923172c12a82e122-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=59db31b32d91b593d872c997b35a92dd) 3. Choose where the code lives. **Deploy and edit on Bunny.net** lets you write code directly in the browser and suits simple scripts. Select **Deploy and edit with GitHub** to keep the code in a GitHub repository. ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/4c633aa635c71ca5a4587cde6c284e77664ec14c081a643591c078701c3fc225-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=f39a6ebce3977e595fff2fa02bf996ce) 4. In the second step of script creation, fill in the **Script Name**. ![](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/fce102df7db0237f53c0b7c8ddbcd3683650997efa76b691e6c771bf4e551624-image.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=9611cef8975a0b12b9ef9e6caf9dd4d4) 5. Select between Standalone or Middleware script type. For more information, see our [Script Types documentation](/docs/scripting). ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/a1f24d73bb4d2b69a77282030761e5bb36419ee969bc94afaf1da90f8a73a5d8-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=97389a178300b8d43a06ee4d943e3c89) 6. Use the Repository section to connect the script with the GitHub repository. If you haven't connected your GitHub account yet, click Connect GitHub Account. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/9875891ba431d9019d9936c6e322e6d7191649942029810dbcc2e8b878f3e930-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=9fccabf9fc45fa16b149f2e197175f78) 7. You will be redirected to the GitHub application permission page, where you can select the repositories that bunny.net can access. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/b49ea810738a3ba548ff9c935f58dbf764a18080ca3b9ac1ea4192761717353c-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=1ab4ce054793413538d00aa154147716) 8. Select **New Repository** to create a preconfigured repository based on a template. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/883188c29dd1e7b951edc19e6f895b680b2af36cfc18b8360e02e1a3fcbad366-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=04d1dc53f0055929c07d2a4cce788f09) 9. Choose a script template. Each template is a starting setup for a different scenario. For demo purposes, you can choose the **Default empty script**. ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/3401a39467d544003dd71968803d98811c20e47c3294ce25ff59311f7061c32b-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=038cce7c3f2ef0c8d6ed1fde0ed9f36c) 10. Click **Add Script**. A new GitHub repository is created from the template, along with a workflow pipeline that deploys the build output to bunny.net Scripting. The pipeline runs straight away. This takes a few seconds, and you can watch it in the repository's Actions tab. ![](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/d6c147e33b08e9550748abad4cd2346079c4813f67b22e3bd4d39e83e7a5de4a-image.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=83c2fd101aa0466d3d6fb69cc8ce4fd4) 11. Your script is now live and ready to be invoked. > [!NOTE] > > The provided templates are starting points for implementing your own application. Use the documentation available within the GitHub repository to learn how to extend it with your application logic, building custom functionality and features as needed. Once the script is live, the bunny.net dashboard shows how it is doing. You can see the number of requests it has served in real time, along with your production deployment: timestamps, version history, and deployment status. Execution logs are there too, which is where you go when something is failing or slower than you expected. Settings, script updates, and redeployment all happen from the same place. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/7980c971aabe77ef7d11c6409da195b9be79d89d6608fefe87b5150356dbd708-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=3701b624a06357b525ce566829861c96) ## Using an existing repository Use this option when you already have a JavaScript or TypeScript project you want to deploy to Edge Scripting. The repository should be compatible with Edge Scripting, ideally built on Deno or Node.js, and structured so it can integrate with the Edge Scripting deployment GitHub action. Setup matches the previous section. **Follow the steps in Creating a new repository up to Step 7**, then continue here: 1. Select **Existing Repository**. 2. Choose a repository from the list of your existing GitHub repositories. If the repository is not listed, ensure that the bunny.net GitHub app has the appropriate permissions. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/afca99d115e11345ebdca25c6bf09f96672f5a2c47f771c7b9a261ab16189904-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=53a709329c471a387620166bd39d29ac) 3. Next, select a project preset based on the framework used in your project. If needed, adjust the **install command, build command**, and **entry file parameters**. A deployment workflow file will be automatically created in your existing repository based on these parameters. If you prefer to create the deployment workflow file yourself, select the **I will create the GitHub workflow file myself** option. 4. Your script is now live and ready to be invoked. The dashboard gives you the same view as it does for a new repository: live request counts, deployment details and version history, execution logs, and the settings and redeploy controls. ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/29a3192d50245eb62811b2a91819dd80e5fd53fc7abb2d0d24b62558cbb8dce8-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=1bb98deedb424a6a84d65b952c3ead5b) # HTMLRewriter Source: https://bunny.net/docs/scripting/html-rewriter Transform HTML responses on the edge with streaming, selector-based rewriting. ## Overview `HTMLRewriter` lets you modify HTML responses as they stream through your edge script. It parses the HTML on the fly and calls your handler functions when it meets matching elements, comments, or text, so the whole document never has to be buffered in memory. ## Quickstart A short middleware example: ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.13.0"; BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("h1", { element(el) { el.setInnerContent("Modified Title"); }, }) .transform(response); }); ``` ## Constructor ```js theme={null} new HTMLRewriter(options?) ``` **options** `object` **properties** **enableEsiTags** `boolean` (default: false) When `true`, enables processing of ESI ([Edge Side Includes](https://www.w3.org/TR/esi-lang)) tags like ``. ## Methods ### `on(selector, handlers)` Registers handlers for elements matching a CSS selector. Returns `this` for chaining. ```js theme={null} rewriter.on("div.content", { element(el) { /* ... */ }, comments(comment) { /* ... */ }, text(text) { /* ... */ }, }); ``` **selector** `string` *required* A CSS selector. See [supported selectors](#supported-css-selectors) below. **handlers** `ElementHandlers` *required* An object with optional handler functions. Each handler can be sync or async. **properties** **element** `(el: Element) =\> void | Promise\` Called when an opening tag matching the selector is encountered. **comments** `(comment: Comment) =\> void | Promise\` Called for HTML comments within the matched element. **text** `(text: TextChunk) =\> void | Promise\` Called for text content within the matched element. ### `onDocument(handlers)` Registers document-level handlers. Returns `this` for chaining. ```js theme={null} rewriter.onDocument({ doctype(doctype) { /* ... */ }, comments(comment) { /* ... */ }, text(text) { /* ... */ }, end(end) { /* ... */ }, }); ``` **handlers** `DocumentHandlers` *required* **properties** **doctype** `(doctype: Doctype) =\> void | Promise\` Called when the `` declaration is encountered. **comments** `(comment: Comment) =\> void | Promise\` Called for document-level comments (outside any element scope). **text** `(text: TextChunk) =\> void | Promise\` Called for document-level text. **end** `(end: DocumentEnd) =\> void | Promise\` Called when the end of the document is reached. ### `transform(response)` Applies all registered handlers to the response body and returns a new `Response`. ```js theme={null} const transformed = rewriter.transform(response); ``` **response** `Response` *required* The HTTP response to transform. Must not be an error response. **Returns:** A new `Response` with: * The same headers (minus `Content-Length`, since the body length may change) * A streaming body with the transformed HTML *** ## Handler types ### Element Passed to `element` handlers. Represents an HTML opening tag. #### Properties | Property | Type | Description | | - | - | - | | `tagName` | `string` | Tag name (lowercase). Readable and writable. | | `namespaceURI` | `string` | Namespace URI (readonly). | | `removed` | `boolean` | Whether the element has been removed (readonly). | | `attributes` | `IterableIterator<[string, string]>` | Iterable of `[name, value]` pairs (readonly). | #### Attribute methods ##### `getAttribute(name)` Returns the value of the attribute with the given name, or `null` if the attribute does not exist. **name** `string` *required* The attribute name. **Returns:** `string | null` ##### `hasAttribute(name)` Returns whether the element has an attribute with the given name. **name** `string` *required* The attribute name. **Returns:** `boolean` ##### `setAttribute(name, value)` Sets the value of the attribute with the given name. Adds the attribute if it does not exist. **name** `string` *required* The attribute name. **value** `string` *required* The attribute value. **Returns:** `Element`, the element itself, for chaining. ##### `removeAttribute(name)` Removes the attribute with the given name. No-op if the attribute does not exist. **name** `string` *required* The attribute name. **Returns:** `Element`, the element itself, for chaining. Setters return the element itself, so calls can be chained: ```js theme={null} el.setAttribute("class", "new") .setAttribute("id", "main") .removeAttribute("style"); ``` #### Content mutation methods All content mutation methods accept `content` as a `string`, `ReadableStream`, or `Response`, and an optional `options` object. They all return `Element` for chaining. **content** `string | ReadableStream\ | Response` *required* The content to insert. Strings are inserted directly. Streams and Response bodies are consumed and piped into the output. **options.html** `boolean` (default: false) When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. ##### `before(content, options?)` Inserts content immediately before the element's opening tag. **Returns:** `Element` ##### `after(content, options?)` Inserts content immediately after the element's closing tag. **Returns:** `Element` ##### `prepend(content, options?)` Inserts content at the beginning of the element, right after the opening tag. **Returns:** `Element` ##### `append(content, options?)` Inserts content at the end of the element, right before the closing tag. **Returns:** `Element` ##### `replace(content, options?)` Replaces the entire element (opening tag, content, and closing tag) with the provided content. **Returns:** `Element` ##### `setInnerContent(content, options?)` Replaces the element's inner content, keeping the opening and closing tags. **Returns:** `Element` ```js theme={null} el.before("
", { html: true }) .setInnerContent("Hello") .after("
", { html: true }); ``` #### Removal methods ##### `remove()` Removes the element and all of its content (opening tag, children, closing tag). **Returns:** `Element` ##### `removeAndKeepContent()` Removes the element's opening and closing tags but keeps the inner content in place. **Returns:** `Element` #### End tag handler ##### `onEndTag(handler)` Registers a handler that is called when the element's closing tag is encountered. **handler** `(endTag: EndTag) =\> void | Promise\` *required* A callback receiving the [`EndTag`](#endtag) object. Can be async. **Returns:** `void` ```js theme={null} el.onEndTag((endTag) => { endTag.before("
", { html: true }); }); ``` ### Comment Passed to `comments` handlers. #### Properties | Property | Type | Description | | - | - | - | | `text` | `string` | The comment text, without ``. Readable and writable. | | `removed` | `boolean` | Whether the comment has been removed (readonly). | #### Methods ##### `before(content, options?)` Inserts content immediately before the comment. **content** `string | ReadableStream\ | Response` *required* The content to insert. **options.html** `boolean` (default: false) When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `Comment` ##### `after(content, options?)` Inserts content immediately after the comment. **content** `string | ReadableStream\ | Response` *required* The content to insert. **options.html** `boolean` (default: false) When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `Comment` ##### `replace(content, options?)` Replaces the comment with the provided content. **content** `string | ReadableStream\ | Response` *required* The content to replace with. **options.html** `boolean` (default: false) When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `Comment` ##### `remove()` Removes the comment from the document. **Returns:** `Comment` ### TextChunk Passed to `text` handlers. Note: a single text node may be split across multiple chunks. #### Properties | Property | Type | Description | | - | - | - | | `text` | `string` | The text content (readonly). | | `lastInTextNode` | `boolean` | `true` if this is the last chunk in the text node (readonly). | | `removed` | `boolean` | Whether this chunk has been removed (readonly). | #### Methods ##### `before(content, options?)` Inserts content immediately before the text chunk. **content** `string | ReadableStream\ | Response` *required* The content to insert. **options.html** `boolean` (default: false) When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `TextChunk` ##### `after(content, options?)` Inserts content immediately after the text chunk. **content** `string | ReadableStream\ | Response` *required* The content to insert. **options.html** `boolean` (default: false) When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `TextChunk` ##### `replace(content, options?)` Replaces the text chunk with the provided content. **content** `string | ReadableStream\ | Response` *required* The content to replace with. **options.html** `boolean` (default: false) When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `TextChunk` ##### `remove()` Removes the text chunk from the document. **Returns:** `TextChunk` ### EndTag Passed to `onEndTag` handlers. #### Properties | Property | Type | Description | | - | - | - | | `name` | `string` | The end tag name. Readable and writable. | #### Methods ##### `before(content, options?)` Inserts content immediately before the end tag. **content** `string | ReadableStream\ | Response` *required* The content to insert. **options.html** `boolean` (default: false) When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `EndTag` ##### `after(content, options?)` Inserts content immediately after the end tag. **content** `string | ReadableStream\ | Response` *required* The content to insert. **options.html** `boolean` (default: false) When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `EndTag` ##### `remove()` Removes the end tag from the document. **Returns:** `EndTag` ### Doctype Passed to `doctype` handlers. All properties are read-only. #### Properties | Property | Type | Description | | - | - | - | | `name` | `string \| null` | The doctype name (e.g. `"html"`). | | `publicId` | `string \| null` | The PUBLIC identifier. | | `systemId` | `string \| null` | The SYSTEM identifier. | ### DocumentEnd Passed to `end` handlers. #### Methods ##### `append(content, options?)` Appends content at the end of the document. **content** `string` *required* The content to append. Only accepts `string` (not streams or responses). **options.html** `boolean` (default: false) When `true`, content is inserted as raw HTML. When `false`, content is escaped as text. **Returns:** `DocumentEnd` ```js theme={null} end.append("", { html: true }); ``` *** ## Supported CSS selectors The following CSS selectors are supported, based on the [W3C Selectors Level 4](https://www.w3.org/TR/selectors-4/) specification. | Selector | Description | Spec | | - | - | - | | `*` | Any element | [Universal selector](https://www.w3.org/TR/selectors-4/#the-universal-selector) | | `E` | Element of type `E` | [Type selector](https://www.w3.org/TR/selectors-4/#type-selectors) | | `E.class` | Element with class | [Class selector](https://www.w3.org/TR/selectors-4/#class-html) | | `E#id` | Element with ID | [ID selector](https://www.w3.org/TR/selectors-4/#id-selectors) | | `E:nth-child(n)` | The n-th child of its parent | [:nth-child()](https://www.w3.org/TR/selectors-4/#the-nth-child-pseudo) | | `E:first-child` | First child of its parent | [:first-child](https://www.w3.org/TR/selectors-4/#the-first-child-pseudo) | | `E:nth-of-type(n)` | The n-th sibling of its type | [:nth-of-type()](https://www.w3.org/TR/selectors-4/#the-nth-of-type-pseudo) | | `E:first-of-type` | First sibling of its type | [:first-of-type](https://www.w3.org/TR/selectors-4/#the-first-of-type-pseudo) | | `E:not(s)` | Element that does not match compound selector `s` | [:not()](https://www.w3.org/TR/selectors-4/#negation) | | `E[attr]` | Element with attribute `attr` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E[attr="value"]` | Attribute exactly equals `value` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E[attr="value" i]` | Case-insensitive attribute match | [Case sensitivity](https://www.w3.org/TR/selectors-4/#attribute-case) | | `E[attr="value" s]` | Case-sensitive attribute match | [Case sensitivity](https://www.w3.org/TR/selectors-4/#attribute-case) | | `E[attr~="value"]` | Whitespace-separated list containing `value` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E[attr^="value"]` | Attribute starts with `value` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E[attr$="value"]` | Attribute ends with `value` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E[attr*="value"]` | Attribute contains `value` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E[attr\|="value"]` | Hyphen-separated attribute starting with `value` | [Attribute selector](https://www.w3.org/TR/selectors-4/#attribute-selectors) | | `E F` | `F` descendant of `E` | [Descendant combinator](https://www.w3.org/TR/selectors-4/#descendant-combinators) | | `E > F` | `F` direct child of `E` | [Child combinator](https://www.w3.org/TR/selectors-4/#child-combinators) | *** ## Examples ### Rewrite links ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.13.0"; BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("a[href]", { element(el) { const href = el.getAttribute("href"); if (href?.startsWith("http://")) { el.setAttribute("href", href.replace("http://", "https://")); } }, }) .transform(response); }); ``` ### Inject a script ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.13.0"; BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .onDocument({ end(end) { end.append('', { html: true }); }, }) .transform(response); }); ``` ### Remove elements ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.13.0"; BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("script[src*='tracker']", { element(el) { el.remove(); }, }) .on(".cookie-banner", { element(el) { el.remove(); }, }) .transform(response); }); ``` ### Async handler Handlers can return a `Promise` for async operations like sub-requests. ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.13.0"; BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("include[src]", { async element(el) { const src = el.getAttribute("src"); const partial = await fetch(src); el.replace(partial.body, { html: true }); }, }) .transform(response); }); ``` ### Class-based handlers Instead of inline objects, you can define handler classes and pass instances to `.on()` or `.onDocument()`. ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.13.0"; class AttributeRewriter { #attrName; constructor(attrName) { this.#attrName = attrName; } element(el) { const value = el.getAttribute(this.#attrName); if (value) { el.setAttribute(this.#attrName, value.replace("http://", "https://")); } } } BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("a", new AttributeRewriter("href")) .on("img", new AttributeRewriter("src")) .transform(response); }); ``` #### Document handler class ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.13.0"; class StripComments { comments(comment) { comment.remove(); } end(end) { end.append("", { html: true }); } } BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .onDocument(new StripComments()) .transform(response); }); ``` #### Async class handler Class methods can be `async` just like inline handlers. ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.13.0"; class IncludeExpander { async element(el) { const src = el.getAttribute("src"); if (src) { const partial = await fetch(src); el.replace(partial.body, { html: true }); } } } BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("include[src]", new IncludeExpander()) .transform(response); }); ``` ### Multiple selectors Chain multiple `.on()` calls to handle different elements independently. ```js theme={null} import * as BunnySDK from "https://esm.sh/@bunny.net/edgescript-sdk@0.13.0"; BunnySDK.net.http.servePullZone() .onOriginResponse(async ({ response }) => { return new HTMLRewriter() .on("title", { element(el) { el.setInnerContent("My Site"); }, }) .on("meta[name='description']", { element(el) { el.setAttribute("content", "Custom description"); }, }) .on("img", { element(el) { el.setAttribute("loading", "lazy"); }, }) .transform(response); }); ``` ## References * [Edge Side Includes](https://www.w3.org/TR/esi-lang) - W3 Reference of ESI. * [W3C Selectors 4 Specification](https://www.w3.org/TR/selectors-4/) * [WASM HTMLRewriter Implementation](https://github.com/remorses/htmlrewriter) - If you want to use locally # HTTPS & SSL Certificates Source: https://bunny.net/docs/scripting/https-ssl-certificates How Edge Scripts verify TLS certificates when fetching external URLs and Pull Zone origins, and how to fix common certificate errors. ## Overview When your script calls `fetch()` on an `https://` URL, or when the runtime fetches your Pull Zone origin, it opens a fully verified TLS connection from the bunny.net edge to that host. It performs the same checks a browser does: the certificate must chain to a trusted public Certificate Authority, be valid for the hostname you connected to, and not be expired. Most of the time this needs no thought. Fetching an external HTTPS URL works out of the box: ```typescript theme={null} import * as BunnySDK from "npm:@bunny.net/edgescript-sdk@0.13.0"; BunnySDK.net.http.servePullZone(async (request: Request): Promise => { const res = await fetch("https://api.example.com/data.json"); return new Response(await res.text()); }); ``` Wrap the call in `try/catch` so that a certificate problem on the origin becomes a response you control instead of an unhandled error: ```typescript theme={null} try { const res = await fetch("https://api.example.com/data.json"); return new Response(await res.text()); } catch (err) { console.error(err); return new Response("Upstream unavailable", { status: 502 }); } ``` > [!NOTE] > > If a `fetch()` fails on the certificate, it rejects with a `TypeError` whose message names the problem, for example `invalid peer certificate: UnknownIssuer`. The [troubleshooting](#troubleshooting) section is organized by that message. ## Handled Automatically Some origins are misconfigured in ways that every browser tolerates. The runtime handles these for you so that a fetch behaves the way it does in a browser. None of them weaken verification: the certificate chain is still checked against trusted roots on every connection, and a certificate that fails for a real reason (untrusted issuer, expired, wrong hostname) still fails. * **Missing intermediate certificates.** Many servers send only their own leaf certificate and omit the intermediate that links it to the root CA. When a certificate fails *only* for this reason, the runtime completes the chain from a built-in list of publicly disclosed intermediates, sourced from the [Common CA Database (CCADB)](https://www.ccadb.org/), then verifies the completed chain against the trusted roots. * **bunny.net edge hostnames without their own certificate.** A hostname pointed at the bunny.net edge that has no SSL provisioned for that specific name (a white-label or alias domain, often reached after a redirect) is answered with one of bunny.net's own edge certificates, such as `*.b-cdn.net`. When verification fails *only* on the hostname check and the certificate provably belongs to bunny.net, the connection is accepted. A mismatched third-party certificate is never accepted this way. * **Older TLS 1.2 origins.** Legacy handshakes that some strict clients reject (for example an ECDSA P-256 certificate whose handshake is signed with SHA-384) connect normally. You do not need to configure anything for these. > [!NOTE] > > These conveniences apply to `fetch()` and origin connections only. The lower-level [`node:tls`](/docs/scripting/node-tls) module performs standard, stricter verification and rejects both cases. ## Private Certificate Authorities The built-in intermediate list only covers certificates disclosed in the CCADB. If your origin uses a private or internal Certificate Authority, trust its certificate for that fetch by passing it to `Deno.createHttpClient`: ```typescript theme={null} const client = Deno.createHttpClient({ caCerts: [ `-----BEGIN CERTIFICATE----- ...your CA certificate (PEM)... -----END CERTIFICATE-----`, ], }); const res = await fetch("https://internal.example.com/", { client }); ``` The certificate you supply is added to the trust store for that client only. Hostname and expiry checks still apply: this adds a trust anchor, it does not disable verification. ## Client Certificates (mTLS) Some origins require the client to present its own certificate. Pass the PEM-encoded certificate and private key to `Deno.createHttpClient` and use that client for the fetch. Store both as [environment secrets](/docs/scripting/secrets) so they never appear in your source code: ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk@0.13.0"; import process from "node:process"; const client = Deno.createHttpClient({ cert: process.env.ClientCert, // certificate presented to the origin (PEM) key: process.env.ClientKey, // matching private key (PEM) // caCerts: [ ... ] // add if the origin uses a private CA }); BunnySDK.net.http.servePullZone(async (request: Request): Promise => { try { const res = await fetch("https://mtls.example.com/status", { client }); return new Response(await res.text(), { status: res.status }); } catch (err) { return new Response(`mTLS request failed: ${err.message}`, { status: 502 }); } }); ``` The origin's own certificate is still verified as usual. > [!NOTE] > > If you have trouble storing a multi-line PEM value in a secret, store it base64-encoded instead and decode it in the script with `atob(process.env.ClientCert)`. ## Your Pull Zone Origin Your pullzone origin fetch has their own configuration in the Pullzone. Those do not affects `fetch()` calls to other URLs. ## Troubleshooting > **UnknownIssuer** > > The origin's certificate does not chain to a public Certificate Authority. The usual cause is a private or internal CA, or an intermediate that is not disclosed in the CCADB. Trust the CA with [`Deno.createHttpClient({ caCerts })`](#private-certificate-authorities). Public origins that merely omit a disclosed intermediate already work automatically. > **NotValidForName** > > The certificate is valid, but not for the hostname you connected to. bunny.net's own edge certificates are accepted automatically. > **Expired** > > The origin's certificate has expired. Renew it on the origin. Expiry cannot be bypassed for external origins. > **421 Misdirected Request** > > An HTTPS origin reached by IP address received no matching server name. > **Works in a browser but fails in a script** > > Browsers hide certificate-chain and hostname problems that a strict client will not. Check the origin's certificate with `openssl`: > > ```bash theme={null} > openssl s_client -connect your-origin.example.com:443 \ > -servername your-origin.example.com > ``` > > `unable to verify the first certificate` means a missing intermediate: fix the origin to send its full chain, or use `caCerts` if the CA is private. `Verify return code: 0 (ok)` means the chain is fine and the problem lies elsewhere. > **Your own origin uses a self-signed certificate** > > Turn off **Verify Origin SSL** on the Pull Zone. This applies only to your configured origin. ## References * [`fetch()` on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch) * [`Deno.createHttpClient`](https://docs.deno.com/api/deno/~/Deno.createHttpClient) * [Common CA Database (CCADB)](https://www.ccadb.org/) * [Node:TLS](/docs/scripting/node-tls) # Bunny Edge Scripting Source: https://bunny.net/docs/scripting/index Run JavaScript and TypeScript at the edge. Build APIs, dynamic UIs, AI-powered functions, or extend Bunny CDN with middleware, all without managing servers. ![bunny.net Scripting](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/scripting/scripting.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=7af372b01d80183b289100960e6adc3a) Bunny Edge Scripting lets you deploy code directly to the edge, close to your users, without running your own servers. > **[Edge Scripting Quickstart](/docs/scripting/quickstart)** Edge Scripting runs on [Deno](https://deno.com/) and V8, the engine behind Chrome. Each script gets an isolated runtime with access to standard Web APIs. You can write the code in the browser or connect a GitHub repository for continuous deployment. ## Script types > **[Standalone Scripts](/docs/scripting/standalone/overview)** > > Independent scripts that handle HTTP requests directly. Build REST APIs, > serve dynamic HTML, process data, or call external services like AI models, > all without an origin server. > **[Middleware Scripts](/docs/scripting/middleware/overview)** > > Intercept and modify requests and responses as they flow through Bunny CDN. > Add authentication, manipulate headers, transform content, or implement > custom caching logic. ## Features * Write, test, and deploy scripts in the browser editor * Connect a GitHub repository for version control and automatic deployments through GitHub Actions * Store API keys and configuration as environment variables or secrets * Debug with live logs and track execution metrics ## Next steps > **[Quickstart](/docs/scripting/quickstart)** > > Create and deploy your first edge script in minutes. > **[Examples](/docs/scripting/standalone/examples/return-json)** > > See practical examples for standalone and middleware scripts. # Limits Source: https://bunny.net/docs/scripting/limits Execution limits for Edge Scripts, covering CPU time, memory, subrequests, script size, and environment variables. | Feature | Limit | Description | | - | - | - | | CPU Time per request | 30s | Maximum CPU time allocated per request. | | Active memory | 128 MB | Maximum memory available to isolate instance. | | Subrequests | 50 | Number of subrequests a script can make per request. | | Script size | 10 MB | Maximum size of the script code. | | Startup time | 500ms | Maximum time to startup the script. | | Environment variables per Script | 128 | Maximum number of environment variables per script. | | Environment variable size | 2KB | Maximum size of environment variable value. | These limits cover typical use under normal operating conditions, and they keep one script from taking resources away from everyone else's. Occasional spikes above a limit are tolerated. A script that consistently hits or exceeds one will be throttled or terminated. ## Staying within the limits * Review your script's code so it finishes within the time and memory limits. * Keep the number of subrequests down, and cache responses where you can. * Watch the logs and statistics for your script so you catch a growing problem before it turns into a limit violation. ## CPU Time CPU time is measured while the script is actually using the CPU to run your logic, and it directly affects how quickly a script can respond. It is allocated per request, so every execution gets the same share of processing time. Infrequent spikes in CPU usage are fine. Going over the limit again and again will lead to the script being terminated. # Logs Source: https://bunny.net/docs/scripting/logs See the messages and errors your script writes while it runs. Logs record the messages, errors, and other runtime information your script produces, which is usually the quickest way to work out why it behaved the way it did. To view the logs, select the **Logs** tab in the navigation pane: ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/1bedd5e1cc124d4756e701543e1a0fa0b3215c07fb8e693eab192f9790bc9543-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=ddd0e0c8614b7ad68441036a3d94666f) # Modify response body (Middleware) Source: https://bunny.net/docs/scripting/middleware/examples/modify-body Use a middleware script to change the response body coming back from the origin before it is cached. This middleware reads the HTML response from the origin and appends `- bunny.net` to the contents of the `` tag, so every page carries it. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; /** * When a response is not served from the cache, you can use this event handler * to modify the response coming from the origin. This runs before the response * is cached. * * Returns an HTTP response. * @param {Context} context - The context of the middleware. * @param {Request} request - The current request done to the origin. * @param {Response} response - The HTTP response or string. */ async function onOriginResponse(context: { request: Request; response: Response; }): Promise<Response> | Response | void { let body = await context.response.text(); body = body.replace("", "- bunny.net"); // Remove the original Content headers so the response is returned correctly const headers = new Headers(context.response.headers); headers.delete("Content-Length"); headers.delete("Content-Encoding"); return new Response(body, { status: context.response.status, // Preserve the original status code headers, // Preserve the original headers }); } BunnySDK.net.http.servePullZone().onOriginResponse(onOriginResponse); ``` # Modify HTTP headers Source: https://bunny.net/docs/scripting/middleware/examples/modify-headers Reject requests that use the wrong method and set your own headers on the response. This script answers `GET` with a JSON body and rejects every other method with a 405 and an `Allow` header. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { const url = new URL(request.url); if (request.method !== "GET") return new Response("Not Allowed", { status: 405, headers: { Allow: "GET", }, }); const responseObj = { hello: "world", }; return new Response(JSON.stringify(responseObj), { headers: { "Content-type": "application/json", }, }); }, ); ``` # Middleware scripts Source: https://bunny.net/docs/scripting/middleware/overview Middleware scripts allow you to transform and modify HTTP requests and responses as they flow through the CDN. Manipulate requests before they reach your origin server or the cache and alter responses before they are sent back to the client. Middleware scripts sit inside the CDN request flow. Your logic runs on requests on their way in and on responses on their way out, so you can change how traffic behaves without touching your backend, and the work happens at the edge rather than on your origin. ## Use cases * Verify credentials and manage session tokens at the edge * Add, modify, or remove HTTP headers on requests and responses * Rewrite HTML, inject scripts, or otherwise change the response body before delivery * Run A/B tests and feature flags, routing users based on headers or cookies * Route or redirect requests based on path, geolocation, or your own logic * Apply rate limiting, IP filtering, or bot protection ## The `servePullZone` function The `servePullZone` function creates a middleware handler that integrates with your Pull Zone. It returns a chainable object for adding request and response middleware. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http .servePullZone({ url: "https://your-origin.com" }) .onOriginRequest((ctx) => { // Modify or intercept requests return Promise.resolve(ctx.request); }) .onOriginResponse((ctx) => { // Modify responses return Promise.resolve(ctx.response); }); ``` ### Function signature ```ts theme={null} servePullZone(options: { url: string }): PullZoneHandler ``` | Option | Type | Description | | - | - | - | | `url` | `string` | The origin URL to proxy requests to. Only used during local development; in production, the Pull Zone origin is used. | > [!NOTE] > > The `url` option is only used during local development. When deployed to > bunny.net, requests are proxied to the origin configured in your Pull Zone > settings. ## Middleware methods The `servePullZone` function returns a `PullZoneHandler` object with chainable middleware methods: ### `onClientRequest` > [!NOTE] > > If your PullZone is configured to execute script before cache, you'll have > access to this function. Intercepts requests before they are sent to the cache. You can modify the request or short-circuit by returning a response directly. ```ts theme={null} onClientRequest( middleware: (ctx: { request: Request }) => Promise | Response | Promise | Request | void ): PullZoneHandler ``` | Property | Type | Description | | - | - | - | | `ctx.request` | `Request` | The incoming request object | **Return value:** * Return `Promise` to continue to the origin with the (modified) request * Return `Promise` to short-circuit and respond immediately without hitting the cache ### `onClientResponse` > [!NOTE] > > If your PullZone is configured to execute script before cache, you'll have > access to this function. Intercepts responses before they are returned to the user, including responses served from the cache. ```ts theme={null} onClientResponse( middleware: (ctx: { request: Request; response: Response }) => Promise | Response ): PullZoneHandler ``` | Property | Type | Description | | - | - | - | | `ctx.request` | `Request` | The original request object | | `ctx.response` | `Response` | The response from the origin server | **Return value:** * Return `Promise` or `Response` with the (modified) response to send to the client. ### `onOriginRequest` Intercepts requests before they are sent to the origin server. You can modify the request or short-circuit by returning a response directly. ```ts theme={null} onOriginRequest( middleware: (ctx: { request: Request }) => Promise | Promise ): PullZoneHandler ``` | Property | Type | Description | | - | - | - | | `ctx.request` | `Request` | The incoming request object | **Return value:** * Return `Promise` to continue to the origin with the (modified) request * Return `Promise` to short-circuit and respond immediately without hitting the origin ### `onOriginResponse` Intercepts responses from the origin server before they are sent to the client. Modifications occur before the response is cached. ```ts theme={null} onOriginResponse( middleware: (ctx: { request: Request; response: Response }) => Promise ): PullZoneHandler ``` | Property | Type | Description | | - | - | - | | `ctx.request` | `Request` | The original request object | | `ctx.response` | `Response` | The response from the origin server | **Return value:** * Return `Promise` with the (modified) response to send to the client ## Enable before cache scripts Before cache execution is turned on per Pull Zone. Navigate to your Pull Zone, then go to **General** > **Origin** and enable **Run script before cache**. > [!NOTE] > > Learn more about [before cache execution](/docs/scripting/before-cache). ## Workflow When a client makes a request to a Pull Zone, the request passes through middleware at different stages: > [!NOTE] > > If your PullZone is configured to execute script before cache, you'll run the > `onClientRequest` and `onClientResponse` if those are registered. 1. **`onClientRequest`** - Called before the request is sent to the cache. Modify the request or return a response to short-circuit. 2. **`onOriginRequest`** - Called before the request is sent to the origin, so only when the cache returns a *MISS*. Modify the request or return a response to short-circuit. 3. **Origin fetch** - The request is sent to your origin server. 4. **`onOriginResponse`** - Called after the origin responds. Modify the response before it's sent to the client and cached. 5. **`onClientResponse`** - Called just before a response is sent to the client, unless you short-circuited at the `onClientRequest` layer. ```mermaid theme={null} sequenceDiagram participant Client participant Cache participant Origin Client->>Cache: onClientRequest Cache->>Origin: onOriginRequest Origin-->>Cache: onOriginResponse Cache-->>Client: onClientResponse ``` ## Example This example gates a route behind a feature flag and adds a custom header to responses: ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http .servePullZone({ url: "https://echo.free.beeceptor.com/" }) .onOriginRequest((ctx) => { const optFT = ctx.request.headers.get("feature-flags"); const featureFlags = optFT ? optFT.split(",").map((v) => v.trimStart()) : []; // Route-based matching and feature flag check const path = new URL(ctx.request.url).pathname; if (path === "/d") { if (!featureFlags.includes("route-d-preview")) { // Short-circuit: return response without hitting origin return Promise.resolve( new Response("You cannot use this route.", { status: 400 }), ); } } // Continue to origin with the request return Promise.resolve(ctx.request); }) .onOriginResponse((ctx) => { // Add custom header to response ctx.response.headers.append("X-Via", "MyMiddleware"); return Promise.resolve(ctx.response); }); ``` ### Local development You can run middleware scripts locally using Deno: ```bash theme={null} deno run -A script.ts ``` Test with curl: ```bash theme={null} # Blocked - missing required feature flag curl http://127.0.0.1:8080/d --header 'feature-flags: something-else' # Allowed - has required feature flag curl http://127.0.0.1:8080/d --header 'feature-flags: route-d-preview, something-else' ``` # Node:FS Source: https://bunny.net/docs/scripting/node-fs Read and write files in Edge Scripts using the Node.js-compatible file system API > [!WARNING] > > No breaking changes are expected, but additional features will be implemented soon. ## Overview Edge Scripting supports the Node.js file system API through the `node:fs` and `node:fs/promises` modules, so you can read, write, and manipulate files and directories from inside an Edge Script. The implementation sits on Deno's sandboxed environment with Node.js compatibility. Both the Promise-based API (`node:fs/promises`) and the older callback-based API (`node:fs`) work. We recommend the Promise-based one for the cleaner async/await syntax. ## Virtual file system Every script gets a virtual file system for file operations. It lives in the virtual memory available to the worker. The directory tree currently has two folders: ``` / # Root directory ├── home/ │ └── user/ # Your current working directory, R/W access └── tmp/ # Temporary directory, R/W access ``` You can create directories anywhere. Permissions are enforced for the owner (user) but not for group or others. All files are opened in `w+`. > [!NOTE] > > Avoid creating directories in the root directory `/`. We plan to add folders > there for other functions, such as `/dev/*` and other read only directories. Paths in the virtual file system are limited to **4096 characters** (`foo/bar` counts as 7) and to **48 segments** (`a/b/c` is 3 segments). Capacity checks for creating, copying and moving happen before the operation runs. Copying a large file can therefore fail with an out of memory error even though there is still space to create new files or directories. ## Importing the module EdgeScripting supports both the Promise-based API and the callback-based API. ### Promise-based API (recommended) ```typescript theme={null} import * as fs from "node:fs/promises"; // Use with async/await const data = await fs.readFile("/tmp/file.txt", "utf-8"); ``` ### Callback-based API ```typescript theme={null} import * as fs from "node:fs"; // Use with callbacks fs.readFile("/tmp/file.txt", "utf-8", (err, data) => { if (err) { console.error("Error:", err); return; } console.log("Data:", data); }); ``` ## Limitations The file system runs in a sandbox, which constrains what it can do. The following sections cover where it differs from Node.js. > [!NOTE] > > Always handle file system errors gracefully. The sandboxed environment may > behave differently from traditional Node.js environments. ### Symbolic links and hard links Symbolic links are supported. `symlink()`, `symlinkSync()`, `readlink()`, `readlinkSync()`, and `lstat()` work as expected. > [!WARNING] > > Hard links are not supported. `link()` and `linkSync()` will throw errors. ### File statistics and timestamps All `Stats` timestamp properties are supported and return correct values: **Reliable Stats properties:** * `size` - File size in bytes * `isFile()` - Check if entry is a file * `isDirectory()` - Check if entry is a directory * `atime` - Last access time * `mtime` - Last modification time * `ctime` - Last status change time * `atimeMs`, `mtimeMs`, `ctimeMs` - Millisecond timestamps * `birthtime` - File creation time ### File and directory permissions File and directory permissions are supported for the **owner (user)** bits. `chmod()` and `chmodSync()` work, and `Stats.mode` reflects the permissions you set. `access()` respects owner-level read, write, and execute bits. > [!NOTE] > > **Group and others** permission bits are accepted but have no effect: the sandboxed environment always runs as the file owner (uid=1000, gid=1000), so only the user/owner bits matter. > > `chown()` and `chownSync()` are implemented for compatibility, but ownership is not enforced. Changing the uid or gid has no effect on file access or permissions. ### File watching File system watching is not supported. > [!WARNING] > > The following operations are unavailable: > > * `watch()` - Watch for file changes > * `watchFile()` - Poll for file changes > * `unwatchFile()` - Stop watching > * `FSWatcher` class **Workaround:** Use polling with `stat()` if you need to detect changes, but be mindful of performance implications and CPU time limits. ### Performance considerations > [!NOTE] > > **Memory limits:** The whole Virtual FileSystem lives inside your script memory. > The current file system limits of your script is set up to 64MB. > Reading large files may cause memory exhaustion if you store it in memory. > Leverage streaming to avoid allocating too much memory at a time. > > **CPU time:** File I/O counts toward the 30-second CPU time limit per request. > > **Best practices:** > > * Stream large files instead of reading entirely into memory > * Clean up temporary files to avoid storage bloat > * See [Limits](/docs/scripting/limits) for more details on resource constraints ## Quickstart Common file system patterns in EdgeScripting. ### Example 1: Reading a file ```typescript theme={null} import * as fs from "node:fs/promises"; import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { try { // Read file as UTF-8 string const content = await fs.readFile("/tmp/data.txt", "utf-8"); return new Response(content, { headers: { "content-type": "text/plain" } }); } catch (error) { return new Response(`Error reading file: ${error.message}`, { status: 500 }); } }); ``` ### Example 2: Writing a file ```typescript theme={null} import * as fs from "node:fs/promises"; import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { const data = await request.json(); try { // Write JSON data to file await fs.writeFile( "/tmp/output.json", JSON.stringify(data, null, 2), "utf-8" ); return new Response("File written successfully", { status: 201 }); } catch (error) { return new Response(`Error writing file: ${error.message}`, { status: 500 }); } }); ``` ### Example 3: Working with directories ```typescript theme={null} import * as fs from "node:fs/promises"; import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { try { // Create directory (recursive option creates parent directories) await fs.mkdir("/tmp/myapp/data", { recursive: true }); // Write a file in the new directory await fs.writeFile("/tmp/myapp/data/config.json", "{}", "utf-8"); // List directory contents const files = await fs.readdir("/tmp/myapp/data"); return new Response(JSON.stringify({ files }), { headers: { "content-type": "application/json" } }); } catch (error) { return new Response(`Error: ${error.message}`, { status: 500 }); } }); ``` ### Example 4: Error handling patterns ```typescript theme={null} import * as fs from "node:fs/promises"; import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { const filepath = "/tmp/cache/data.txt"; try { // Check if file exists by trying to access it await fs.access(filepath); // File exists, read it const content = await fs.readFile(filepath, "utf-8"); return new Response(content, { headers: { "x-cache": "hit" } }); } catch (error) { if (error.code === "ENOENT") { // File doesn't exist, create it const defaultContent = "Default cached data"; await fs.writeFile(filepath, defaultContent, "utf-8"); return new Response(defaultContent, { status: 201, headers: { "x-cache": "miss" } }); } // Re-throw other errors throw error; } }); ``` ### Example 5: Using FileHandle for chunked reading > [!WARNING] > > This script is not optimized. It shows how chunked reading works, so please > don't run anything like it in production. ```typescript theme={null} import * as fs from "node:fs/promises"; import * as BunnySDK from "@bunny.net/edgescript-sdk"; // Global promise to track the download operation let downloadPromise: Promise | null = null; const filePath = "/tmp/large-file.txt"; async function downloadLoremIpsum() { try { const response = await fetch('https://lorem-api.com/api/lorem', { method: 'GET', headers: { 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const content = await response.text(); // Calculate how many times we need to repeat to reach ~10MB const targetSize = 10 * 1024 * 1024; // 10MB in bytes const contentSize = Buffer.byteLength(content, 'utf-8'); const repeatCount = Math.ceil(targetSize / contentSize); // Create the repeated content let finalContent = ''; for (let i = 0; i < repeatCount; i++) { finalContent += content; } // Write to file await fs.writeFile(filePath, finalContent, 'utf-8'); } catch (error) { // Clear the promise on error so it can be retried downloadPromise = null; throw error; } } async function ensureFileExists() { try { // Check if file exists await fs.access(filePath); } catch (error) { // File doesn't exist, initiate download if (!downloadPromise) { // Start download and store the promise downloadPromise = downloadLoremIpsum(); } // Wait for the download to complete (whether we started it or another request did) await downloadPromise; } } BunnySDK.net.http.serve(async (request: Request) => { try { // Ensure file exists (download if necessary, blocking until complete) await ensureFileExists(); // Create SSE stream const stream = new ReadableStream({ async start(controller) { const encoder = new TextEncoder(); let fileHandle; try { // Open file for reading fileHandle = await fs.open(filePath, "r"); // Get file stats to know the total size const stats = await fileHandle.stat(); const totalSize = stats.size; // Send initial event with file info controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "start", totalSize, chunkSize: 1024, timestamp: new Date().toISOString() })}\n\n`)); const buffer = new Uint8Array(1024); let position = 0; let chunkNumber = 0; // Read file 1KB at a time while (position < totalSize) { const { bytesRead } = await fileHandle.read(buffer, 0, 1024, position); if (bytesRead === 0) break; // End of file // Convert to string const content = new TextDecoder().decode(buffer.slice(0, bytesRead)); // Send chunk as SSE event controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "chunk", chunkNumber, bytesRead, position, content, progress: ((position + bytesRead) / totalSize * 100).toFixed(2) + "%", timestamp: new Date().toISOString() })}\n\n`)); position += bytesRead; chunkNumber++; // Small delay to prevent overwhelming the client await new Promise(resolve => setTimeout(resolve, 10)); } // Send completion event controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "complete", totalChunks: chunkNumber, totalBytesRead: position, timestamp: new Date().toISOString() })}\n\n`)); } catch (error) { // Send error event controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "error", message: error.message, timestamp: new Date().toISOString() })}\n\n`)); } finally { // Close file handle await fileHandle?.close(); controller.close(); } } }); return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no" } }); } catch (error) { return new Response(`Error: ${error.message}`, { status: 500 }); } }); ``` ### Example 6: Symbolic links ```typescript theme={null} import * as fs from "node:fs/promises"; import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { const original = "/tmp/original.txt"; const link = "/tmp/link.txt"; try { await fs.writeFile(original, "hello from original", "utf-8"); // Create a symbolic link pointing to the original file await fs.symlink(original, link); // Reading through the symlink is transparent const content = await fs.readFile(link, "utf-8"); // lstat inspects the link itself, not the target const linkStats = await fs.lstat(link); const target = await fs.readlink(link); return new Response( JSON.stringify({ content, isSymlink: linkStats.isSymbolicLink(), target }), { headers: { "content-type": "application/json" } } ); } catch (error) { return new Response(`Error: ${error.message}`, { status: 500 }); } }); ``` ## References * [Node.js File System Documentation](https://nodejs.org/api/fs.html) - Complete Node.js fs module reference * [Node.js fs/promises API](https://nodejs.org/api/fs.html#promises-api) - Promise-based file system API * [Node.js FileHandle Class](https://nodejs.org/api/fs.html#class-filehandle) - Advanced file operations * [Node.js File System Flags](https://nodejs.org/api/fs.html#file-system-flags) - Available flags for file operations * [MDN File API](https://developer.mozilla.org/en-US/docs/Web/API/File) - Web standard File interface * [MDN Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) - Binary data objects * [MDN TextEncoder](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder) - Encoding strings to bytes * [MDN TextDecoder](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder) - Decoding bytes to strings * [EdgeScripting Limits](/docs/scripting/limits) - Resource limits and quotas # Node:TLS Source: https://bunny.net/docs/scripting/node-tls Open low-level TLS client connections from Edge Scripts with the Node.js-compatible node:tls module, and learn when to use it instead of fetch(). ## Overview Edge Scripting supports the client side of the Node.js `node:tls` module, so a script can open an outbound TLS connection and speak any protocol over it, not just HTTP. It also gives you access to things `fetch()` hides: the peer certificate, the negotiated protocol and cipher, and the ability to present a client certificate or pin a specific server certificate. For HTTP and HTTPS endpoints, prefer `fetch()`. | Use `fetch()` when… | Use `node:tls` when… | | - | - | | Talking to an HTTP or HTTPS endpoint | Speaking a non-HTTP protocol over TLS | | You want browser-like handling origins | You need low-level control of the TLS connection, or need to inspect the certificate | | Recommended for almost all cases | Advanced use | ## Quickstart Opens an outbound TLS connection and reports whether the certificate was trusted. `tls.connect()` verifies the server certificate against the public root CAs; the callback runs once the handshake completes, and `socket.authorized` reports whether the chain was trusted: ```typescript theme={null} import * as BunnySDK from "npm:@bunny.net/edgescript-sdk@0.13.0"; import tls from "node:tls"; BunnySDK.net.http.serve(async (request: Request): Promise => { return new Promise((resolve) => { const socket = tls.connect( { host: "example.com", port: 443, servername: "example.com" }, () => { const info = `authorized=${socket.authorized} protocol=${socket.getProtocol()}`; socket.end(); resolve(new Response(info)); }, ); socket.on("error", (err) => resolve(new Response(`TLS error: ${err.message}`, { status: 502 })), ); }); }); ``` Always set `servername` so the server receives the SNI it needs to select the right certificate. ## Importing the module ```typescript theme={null} import tls from "node:tls"; ``` The module exposes the following client-side API: | Export | Description | | - | - | | `connect()` | Open an outbound TLS client connection. | | `TLSSocket` | The socket type a connection produces. | | `checkServerIdentity()` | Verify that a certificate matches a hostname. | | `createSecureContext()` | Build TLS options for a connection. | | `rootCertificates` | The built-in list of trusted root CAs. | | `setDefaultCACertificates()` | Override the default CA list. | | `getCiphers()` | Returns an array with the names of the supported TLS ciphers. The names are lower-case. | | Constants | `DEFAULT_MIN_VERSION` (`TLSv1.2`), `DEFAULT_MAX_VERSION` (`TLSv1.3`), and the other standard constants. | ## Certificate verification If you must use `node:tls` against an origin with a private CA or a missing intermediate, supply the certificate yourself with the connection's `ca` option: ```typescript theme={null} import tls from "node:tls"; const socket = tls.connect({ host: "internal.example.com", port: 8883, servername: "internal.example.com", ca: [ `-----BEGIN CERTIFICATE----- ...your CA or intermediate certificate (PEM)... -----END CERTIFICATE-----`, ], }); ``` ## Examples ### Example 1: SSL checker `fetch()` verifies certificates but never shows them to you. With `node:tls` you can open the connection yourself, wait for the handshake, and read what the server presented. This script exposes an endpoint that returns a JSON report about a host's certificate: who issued it, which names it covers, when it expires, and whether the chain is trusted. Point a monitor at it to be warned before a certificate runs out. ```typescript theme={null} import * as BunnySDK from "npm:@bunny.net/edgescript-sdk@0.13.0"; import tls from "node:tls"; const WARN_DAYS = 14; interface CertReport { host: string; port: number; authorized: boolean; authorizationError?: string; protocol: string | null; cipher: string; subject?: string; issuer?: string; altNames: string[]; validFrom?: string; validTo?: string; daysUntilExpiry?: number; fingerprint256?: string; warning?: string; } /** * Opens a TLS connection to host:port and resolves with a report about the * certificate once the handshake completes. */ function checkCertificate(host: string, port: number): Promise { return new Promise((resolve, reject) => { const socket = tls.connect( { host, port, servername: host, // Complete the handshake even if verification fails, so we can // report *why* instead of just failing. `authorized` tells us the result. rejectUnauthorized: false, }, () => { const cert = socket.getPeerCertificate(); const validTo = cert?.valid_to ? new Date(cert.valid_to) : undefined; const daysUntilExpiry = validTo ? Math.floor((validTo.getTime() - Date.now()) / 86_400_000) : undefined; const report: CertReport = { host, port, authorized: socket.authorized, authorizationError: socket.authorizationError?.toString(), protocol: socket.getProtocol(), // e.g. "TLSv1.3" cipher: socket.getCipher().name, subject: cert?.subject?.CN, issuer: cert?.issuer?.O ?? cert?.issuer?.CN, altNames: (cert?.subjectaltname ?? "") .split(", ") .filter(Boolean) .map((n) => n.replace(/^DNS:/, "")), validFrom: cert?.valid_from, validTo: cert?.valid_to, daysUntilExpiry, fingerprint256: cert?.fingerprint256, }; if (!socket.authorized) { report.warning = "Certificate chain is not trusted"; } else if (daysUntilExpiry !== undefined && daysUntilExpiry < 0) { report.warning = "Certificate has expired"; } else if (daysUntilExpiry !== undefined && daysUntilExpiry <= WARN_DAYS) { report.warning = `Certificate expires in ${daysUntilExpiry} days`; } socket.end(); resolve(report); }, ); socket.on("error", reject); socket.setTimeout(8000, () => { socket.destroy(); reject(new Error("TLS connection timed out")); }); }); } BunnySDK.net.http.serve(async (request: Request): Promise => { const url = new URL(request.url); const host = url.searchParams.get("host"); const port = Number(url.searchParams.get("port") ?? "443"); if (!host) { return new Response("Missing ?host= parameter", { status: 400 }); } try { const report = await checkCertificate(host, port); return new Response(JSON.stringify(report, null, 2), { // 200 when healthy, 503 when something needs attention, // so uptime monitors can alert on the status code alone. status: report.warning ? 503 : 200, headers: { "content-type": "application/json" }, }); } catch (err) { return new Response( JSON.stringify({ host, port, error: err.message }), { status: 502, headers: { "content-type": "application/json" } }, ); } }); ``` Calling `/?host=example.com` returns something like: ```json theme={null} { "host": "example.com", "port": 443, "authorized": true, "protocol": "TLSv1.3", "cipher": "TLS_AES_256_GCM_SHA384", "subject": "example.com", "issuer": "DigiCert Inc", "altNames": ["example.com", "www.example.com"], "validFrom": "Jan 15 00:00:00 2026 GMT", "validTo": "Jan 15 23:59:59 2027 GMT", "daysUntilExpiry": 151, "fingerprint256": "AB:CD:12:..." } ``` A few notes on the code: * `rejectUnauthorized: false` is used here on purpose. This endpoint only *reports* on a certificate and never sends data to the host, so completing the handshake for an untrusted certificate is safe and lets the report explain the failure through `authorized` and `authorizationError`. Do not use this option when you actually talk to the origin. * Always set `servername` so the server receives the SNI it needs to select the right certificate. Without it, hosts serving several sites return their default certificate. * The endpoint answers `503` when there is a warning, so an uptime monitor can alert on the status code without parsing the body. ### Example 2: Certificate pinning Standard verification accepts any certificate a public CA is willing to issue for a hostname. If you send credentials to a sensitive origin, you may want to go further and only accept the exact certificate you expect. `fetch()` gives you no access to the peer certificate, but `tls.connect()` lets you pass your own `checkServerIdentity` function, which runs during the handshake before any application data is sent. The function below first performs the standard hostname check, then compares the certificate's SHA-256 fingerprint to a pinned value stored as an [environment secret](/docs/scripting/secrets). Returning an `Error` aborts the connection: ```typescript theme={null} import * as BunnySDK from "npm:@bunny.net/edgescript-sdk@0.13.0"; import tls from "node:tls"; import process from "node:process"; const ORIGIN_HOST = "api.example.com"; // SHA-256 fingerprint of the expected certificate, e.g. // "AB:CD:12:...". Get it with: // openssl s_client -connect api.example.com:443 /dev/null \ // | openssl x509 -noout -fingerprint -sha256 const PINNED_FINGERPRINT = process.env.PinnedFingerprint; const API_TOKEN = process.env.ApiToken; function pinnedRequest(path: string): Promise { return new Promise((resolve, reject) => { const chunks: Uint8Array[] = []; const socket = tls.connect( { host: ORIGIN_HOST, port: 443, servername: ORIGIN_HOST, checkServerIdentity(hostname, cert) { // Keep the standard hostname check... const err = tls.checkServerIdentity(hostname, cert); if (err) return err; // ...and additionally require the exact certificate we pinned. if (cert.fingerprint256 !== PINNED_FINGERPRINT) { return new Error( `Certificate fingerprint mismatch: got ${cert.fingerprint256}`, ); } }, }, () => { // Only reached when the pin matched. Safe to send the token. socket.write( `GET ${path} HTTP/1.1\r\n` + `Host: ${ORIGIN_HOST}\r\n` + `Authorization: Bearer ${API_TOKEN}\r\n` + `Connection: close\r\n\r\n`, ); }, ); socket.on("data", (chunk) => chunks.push(chunk)); socket.on("end", () => { const raw = new TextDecoder().decode(concat(chunks)); resolve(raw.split("\r\n\r\n")[1] ?? ""); // body only }); socket.on("error", reject); socket.setTimeout(8000, () => { socket.destroy(); reject(new Error("TLS connection timed out")); }); }); } function concat(chunks: Uint8Array[]): Uint8Array { const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0)); let offset = 0; for (const c of chunks) { out.set(c, offset); offset += c.length; } return out; } BunnySDK.net.http.serve(async (request: Request): Promise => { if (!PINNED_FINGERPRINT || !API_TOKEN) { return new Response("Pin or token not configured", { status: 500 }); } try { const body = await pinnedRequest("/v1/account"); return new Response(body, { headers: { "content-type": "application/json" } }); } catch (err) { return new Response(`Pinned request failed: ${err.message}`, { status: 502 }); } }); ``` Pinning trades flexibility for assurance: when the origin rotates its certificate, the pin must be updated or requests start failing. Pin the fingerprint of a longer-lived intermediate CA (compare `cert.issuerCertificate.fingerprint256`) if the origin renews its leaf certificate often. ### Example 3: Client certificates (mTLS) If the server requires a client certificate, pass the PEM-encoded `cert` and `key` in the connection options, read from [environment secrets](/docs/scripting/secrets): ```typescript theme={null} import process from "node:process"; const socket = tls.connect({ host: "mqtt.example.com", port: 8883, servername: "mqtt.example.com", cert: process.env.ClientCert, key: process.env.ClientKey, }); ``` > [!NOTE] > > If you have trouble storing a multi-line PEM value in a secret, store it base64-encoded instead and decode it in the script with `atob(process.env.ClientCert)`. For HTTPS origins that require mTLS, use `fetch()` with `Deno.createHttpClient` instead. See [Client Certificates on the HTTPS page](/docs/scripting/https-ssl-certificates#client-certificates-mtls). ## References * [`node:tls` on nodejs.org](https://nodejs.org/api/tls.html) * [HTTPS & SSL Certificates](/docs/scripting/https-ssl-certificates) # Pricing Source: https://bunny.net/docs/scripting/pricing How Edge Scripting is billed: CPU time, requests, increments, and worked examples. ## Pricing components Edge Scripting pricing is based on two components: 1. **CPU Time:** CPU Time refers to the actual time the CPU spends processing your script code. This measurement excludes any waiting time for input/output operations, such as fetch requests. CPU Time is measured in milliseconds (ms) and billed in increments of 1 million milliseconds (1000s). 2. **Requests**: This component tracks the number of requests that your script executes. Requests are billed in increments of one million requests. | CPU Time | Requests | | - | - | | \$0.02 / 1000s CPU time | \$0.20 / million requests | Increments are applied based on the total usage across all scripts, rather than for each individual script. Billing begins at the first increment, not at the first million. As soon as a script serves a single request, the first increment of each component is charged in full, and all remaining usage within that increment is already covered. > [!NOTE] > > The smallest monthly Scripting charge is \$0.22. That is the first CPU Time increment (\$0.02) plus the first Requests increment (\$0.20). You will see it as soon as you start testing a script. It stays at \$0.22 until you pass 1,000,000 requests or 1000s of CPU time. See [Example 1: Minimum usage](#example-1-minimum-usage). > [!NOTE] > > CDN bandwidth is charged at the normal rate and billed separately. It is not included in the CPU Time or Requests charges. ## Example pricing scenarios ### Example 1: Minimum usage A newly deployed script that serves a single request, consuming 12 milliseconds of CPU time, would have the following estimated costs: | | Monthly cost | Calculation | | - | - | - | | CPU Time | \$0.02 | 12 ms falls within the first 1,000,000 ms increment \* \$0.02 | | Requests | \$0.20 | 1 request falls within the first 1,000,000 request increment \* \$0.20 | **Total Monthly Cost: \$0.22** The rest of both increments is already paid for. Every further request that month adds nothing to this total until you pass 1,000,000 requests or 1000s of CPU time, at which point the next increment begins. ### Example 2: Moderate usage A script that handles 10 million requests per month, with each request consuming an average of 10 milliseconds of CPU time, would have the following estimated costs: | | Monthly cost | Calculation | | - | - | - | | CPU Time | \$2.00 | 0.01s CPU time per request \* 10,000,000 requests / 1000s \* \$0.02 | | Requests | \$2.00 | 10,000,000 requests / 1,000,000 \* \$0.20 | **Total Monthly Cost: \$4.00** ### Example 3: High usage A script that handles 100 million requests per month, with each request consuming an average of 7 milliseconds of CPU time, would have the following estimated costs: | | Monthly cost | Calculation | | - | - | - | | CPU Time | \$14.00 | 0.007s CPU time per request \* 100,000,000 requests / 1000s \* \$0.02 | | Requests | \$20.00 | 100,000,000 requests / 1,000,000 \* \$0.20 | **Total Monthly Cost: \$34.00** Because billing works in whole increments, your cost stays flat within an increment and steps up only when you cross into the next one. # Quickstart Source: https://bunny.net/docs/scripting/quickstart Create and deploy your first Edge Script in minutes. **1. Login and navigate to scripting** Login to [bunny.net dashboard](https://dash.bunny.net/auth/login?pk_buttonlocation=menu), then navigate to **Edge Platform** > **Scripting** and click **Add Script**. ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/0b5bff4662563e07b358a70392b77a8be85f58aa8e1cd21968b2dfef2f88b34e-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=1a7858cf61f540e09fb1ca3f243e46fe) **2. Select a script type** Provide a name for your script and select a script type: **Standalone** Select **Standalone** type and then **Default standalone** template. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/68f18ff3816d5947d7acd499028840c65c3e6c7dc5461a8e81d1f8807cc3548e-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=571282cd58ea3b9eada68d10162e1bf6) Standalone scripts handle HTTP requests directly at the edge, without an origin server behind them. Use them for REST APIs, UI applications, and data processing. [Learn more about standalone scripts](/docs/scripting/standalone/overview) **Middleware** Select **Middleware** type and then **Default middleware** template. Middleware scripts intercept and modify requests and responses as they flow through the CDN. Use them for authentication, header manipulation, and content transformation. [Learn more about middleware scripts](/docs/scripting/middleware/overview) Click **Add script**. **3. Explore the script editor** A pull zone is created for your script, so it is reachable from the outside through a unique URL. The script starts with a placeholder function that returns "Hello from" when you open that URL. ![](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/da2454f3d77252f3af19246fb567d36fcd0d40a767a564e238f3f481d3f4fcb5-image.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=6a1641ff4c0ea2b5a5b32f7039e3964e) The bottom left panel is a preview pane where you can enter a URL and run the script currently open in the editor. Running it here doesn't change the deployed script, so you can test your code before publishing it. ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/23d1b7ac94614fa713ed6fb811bc5284d46539c1c2b8b0019d7ca6a344c9f7c0-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=44d20a19d7391b42db2dc2bd119a5e44) On the bottom right, the dashboard includes a logs pane. This pane captures and displays output from any `console.log` calls made by the script when it is run in the preview pane. ![](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/d5c773703340b197b87dbc664e48ae7fc0eae31d410d70ad2fca4acbb07994da-image.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=08e18959e76f9eaed2d959e38ab6ea7a) Use it to trace what the script did on the last run and to confirm it behaved the way you expected. **4. Edit and test your script** Replace the placeholder with your own code, then open the script URL to check that your changes work. **5. Save and publish** When you are happy with your script, click **Save** and **Publish**. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/a3a37ccbb4b2fbbad74ad195292713e3875f727a99bb2bba411cc3e7c11e977a-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=6fba8fe7c300ae3b8faf53087b80de9d) Published changes take effect immediately. # Edge Scripting Regions Source: https://bunny.net/docs/scripting/regions The bunny.net points of presence where Edge Scripts run, listed with their PoP codes. Edge Scripts run on a subset of the bunny.net global network. When a request reaches a Pull Zone that has a script attached, the script executes on the nearest point of presence (PoP) that supports Edge Scripting. Requests that reach a PoP without Edge Scripting are forwarded to the nearest supported location before your code runs. ## Available locations Edge Scripting is available on **72 points of presence** as of September 24, 2026\. The network changes over time, so this list may lag a little behind. The table lists every location in alphabetical order, with the country and the PoP code bunny.net uses to identify it. | City | Country | PoP code | | - | - | - | | Adelaide | Australia | `ADL` | | Almaty | Kazakhstan | `KZ` | | Amsterdam | Netherlands | `AMS` | | Ashburn | United States | `ASB` | | Athens | Greece | `GR` | | Auckland | New Zealand | `AUC` | | Baghdad | Iraq | `IQ` | | Bangkok | Thailand | `TH` | | Belgrade | Serbia | `RS` | | Bogota | Colombia | `CO` | | Boston | United States | `BO` | | Brasilia | Brazil | `BS` | | Bratislava | Slovakia | `SK` | | Brisbane | Australia | `BRB` | | Brussels | Belgium | `BE` | | Bucharest | Romania | `BU` | | Budapest | Hungary | `HU` | | Buenos Aires | Argentina | `AR` | | Charlotte | United States | `CLT` | | Chicago | United States | `IL` | | Curitiba | Brazil | `CWB` | | Dallas | United States | `TX` | | Denver | United States | `DEN` | | Dubai | United Arab Emirates | `AE` | | Fortaleza | Brazil | `FO` | | Frankfurt | Germany | `DE` | | Fujairah | United Arab Emirates | `FU` | | Helsinki | Finland | `FI` | | Hong Kong | Hong Kong | `HK` | | Houston | United States | `HOU` | | Istanbul | Türkiye | `TR` | | Jakarta | Indonesia | `ID` | | Johannesburg | South Africa | `JH` | | Kuwait City | Kuwait | `KWI` | | Kyiv | Ukraine | `UA` | | Lima | Peru | `PE` | | Ljubljana | Slovenia | `LJ` | | London | United Kingdom | `UK` | | Los Angeles | United States | `LA` | | Madrid | Spain | `ES` | | Marseille | France | `MS` | | Melbourne | Australia | `MEL` | | Mexico City | Mexico | `MX` | | Miami | United States | `MI` | | Milan | Italy | `IT` | | Minneapolis | United States | `MSP` | | Montreal | Canada | `MN` | | Moscow | Russia | `RU` | | New York City | United States | `NY` | | Paris | France | `FR` | | Perth | Australia | `PER` | | Phoenix | United States | `PHX` | | Porto Alegre | Brazil | `PA` | | Prague | Czechia | `CZ` | | Quito | Ecuador | `EC` | | Riga | Latvia | `LV` | | San Jose | United States | `SIL` | | Sao Paulo | Brazil | `BR` | | Seattle | United States | `WA` | | Seoul | South Korea | `KR` | | Singapore | Singapore | `SG` | | Sofia | Bulgaria | `BG` | | Stockholm | Sweden | `SE` | | Sydney | Australia | `SYD` | | Taipei | Taiwan | `TW` | | Tel Aviv | Israel | `ISR` | | Tokyo | Japan | `JP` | | Toronto | Canada | `CA` | | Vancouver | Canada | `VA` | | Vienna | Austria | `AT` | | Zagreb | Croatia | `HR` | | Zurich | Switzerland | `CH` | ## Finding the PoP that ran your script Every response served through bunny.net carries a `Server` header in the form `BunnyCDN-{PoP code}-{Server ID}`. The PoP code matches the codes in the table above, so you can confirm where a request was handled: ```bash theme={null} curl -sI https://example.b-cdn.net/ | grep -i '^server:' ``` ``` Server: BunnyCDN-UK-12 ``` In this example the request was served by a server in the London (`UK`) location. Inside a script, read the `CDN-ServerZone` request header to get the PoP code of the location running your code: ```ts theme={null} const pop = request.headers.get("CDN-ServerZone"); ``` The [Route by country](/docs/scripting/standalone/examples/geo-routing) example shows how to react to the visitor's location from inside a script. ## Related resources * [Limits](/docs/scripting/limits) - Execution limits that apply on every location * [Deployments](/docs/scripting/deployments) - How a new version reaches every PoP * [Global Network Regions](/docs/cdn/regions) - Coverage of the full CDN network # Runtime Source: https://bunny.net/docs/scripting/runtime The runtime APIs available on the bunny.net platform. The bunny.net EdgeScript Runtime is based on Deno, so you can use a subset of what is available from Deno or Node. On top of that, we provide functions that change how the script behaves in our environment or bind it to other bunny.net services. ## waitUntil The `waitUntil` function extends the life of the isolate running a request. Use it when a script needs to keep working after the request it answered has finished. Even when no other requests are routed to the script, the invocation stays alive. It is useful for holding [WebSocket](./websockets) connections open, refreshing a cache entry in the background, or firing off telemetry once the response has gone back to the client. ### Signature ```typescript theme={null} Bunny.v1.waitUntil(promise: Promise): void; ``` ### Parameters **promise** `Promise\` *required* A promise representing background work. The isolate will stay alive until this promise settles (resolves or rejects). ### Returns `void`. `waitUntil` does not return a value. > [!NOTE] > > You can call `waitUntil` multiple times; the script will only be evicted once > every given promise has been resolved. ### Example Return the response to the client immediately while a slower task, in this case populating the cache, finishes in the background. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request): Promise => { const url = new URL(request.url); const cache = caches.default; const cacheKey = new Request(url.toString(), { method: "GET" }); const hit = await cache.match(cacheKey); if (hit) { return hit; } const fresh = Response.json( { generatedAt: new Date().toISOString(), random: Math.random() }, { headers: { "Cache-Control": "s-maxage=60" } }, ); // Don't block the response on the cache write. Let it finish after we // return; the isolate stays alive until cache.put() resolves. Bunny.v1.waitUntil(cache.put(cacheKey, fresh.clone())); return fresh; }); ``` ## Runtime maintenance and compatibility Edge Scripting runs on our own customized Deno runtime. With Deno winding down active development, we want to reassure you that nothing changes for Edge Scripting. Your existing scripts will continue running, and you can keep developing and deploying new ones as usual. We'll continue maintaining our runtime with V8 upgrades, security fixes, and support for new TypeScript and Node.js features. We're monitoring developments and evaluating our long-term options. Whatever runtime we use in the future, we're committed to preserving compatibility with existing scripts and APIs. We'll share more about our long-term plans well before Deno's support window ends. ## References * [WebSocket](./websockets) * [Cache API](./cache) # Secrets Source: https://bunny.net/docs/scripting/secrets Environment secrets are sensitive configuration settings, such as API keys, passwords, or tokens, that are securely stored and used by your script. Environment secrets hold sensitive configuration values, such as API keys, passwords, or tokens. Your script reads them at runtime, so they never have to appear in your source code. Unlike [environment variables](/docs/scripting/environment-variables), secrets cannot be viewed once they are set. You can only update or delete them, which keeps the value hidden even from people with dashboard access. ## Adding environment secrets To add a secret to your script: 1. Log in to the [bunny.net dashboard](https://dash.bunny.net/auth/login?pk_buttonlocation=menu). 2. Select **Edge Platform**, click on **Scripting**, and select the script. ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/451afdb1a0674dcbb69c85311754df31972470449385a5c1609cd1e7a704bc9a-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=c17387df45712a6e8f05f0cc8b521b51) 3. Select **Env Configuration** and **Environment Secrets**, and fill in the details of the secret you want to create. ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/5242ce0eea550cee54d8f506dbd8165f90598d18cb59f15169eaaa7500780d1e-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=d6c6a508b34d81d592d713cc58b7b764) 4. Click **Save Secret**. ## Using environment secrets Environment secrets are accessed in the same way as environment variables, using either the Node.js process module or `Deno.env`. ```javascript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; import process from "node:process"; BunnySDK.net.http.serve(async (request: Request): Response | Promise => { // Load secret named ApiKey const apiKey = process.env.ApiKey; // Use the secret in an API request const response = await fetch("https://api.example.com/data", { headers: { "Authorization": `Bearer ${apiKey}` } }); return new Response(await response.text()); }); ``` You can also use `Deno.env.get()`: ```javascript theme={null} const apiKey = Deno.env.get("ApiKey"); ``` > [!NOTE] > > Names of environment variables and secrets on the same script must be unique. > You cannot use the same name for both a variable and a secret. # Server-Sent Events Source: https://bunny.net/docs/scripting/server-sent-events Stream Server-Sent Events from an Edge Script, and keep the isolate alive while the connection is open. An Edge Script can answer a request with a stream of [Server-Sent Events](/docs/cdn/server-sent-events). The script returns a `Response` that holds a `ReadableStream`, and the edge delivers each event as the script writes it. The [CDN page](/docs/cdn/server-sent-events) describes how the network treats an event stream, including the heartbeat and the caching rules. This page describes what a script must do. ## Quickstart This example sends one event immediately, and one more every 15 seconds. The interval also acts as the heartbeat that keeps the connection open. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; const HEARTBEAT_MS = 15_000; BunnySDK.net.http.serve(async (request: Request) => { const encoder = new TextEncoder(); let timer: number; let onClosed: () => void; // Resolves when the client goes away and the runtime cancels the stream. const closed = new Promise((resolve) => { onClosed = resolve; }); const stream = new ReadableStream({ start(controller) { let id = 0; const send = () => { id += 1; controller.enqueue( encoder.encode( `id: ${id}\nevent: tick\ndata: ${new Date().toISOString()}\n\n`, ), ); }; // Send the first event at once. The client waits until this byte. send(); timer = setInterval(send, HEARTBEAT_MS); }, cancel() { clearInterval(timer); onClosed(); }, }); // It is important to call waitUntil; otherwise the script may be evicted // while the stream is still open. Bunny.v1.waitUntil(closed); return new Response(stream, { headers: { "content-type": "text/event-stream", "cache-control": "no-cache", }, }); }); ``` ## Return the response before you wait Return the `Response` at once, and let the stream fill in afterwards. A script that waits before it returns is treated as a slow origin: the edge allows 60 seconds for a response to start, and answers with `504 Gateway Timeout` when nothing arrives. This applies to any `await` before the `return`, and a subrequest is the common case. ```typescript theme={null} // Wrong. If the subrequest takes longer than 60 seconds, the client gets a 504 // and never sees the stream. const upstream = await fetch(slowOrigin); return new Response(upstream.body, { headers }); ``` ```typescript theme={null} // Right. The response goes back at once, and the subrequest runs inside the // stream, so a slow origin delays an event instead of failing the request. const stream = new ReadableStream({ async start(controller) { const upstream = await fetch(slowOrigin); // ... write events from the upstream body }, }); return new Response(stream, { headers }); ``` > [!WARNING] > > Write the first event as soon as the stream opens, and do not wait for the > first real update. The client receives no part of the response until your > script writes the first byte, and this includes the response headers. ## Keep the isolate alive The isolate that answers a request can be evicted once the handler returns. An event stream lives longer than the handler, so pass a promise to [`waitUntil`](/docs/scripting/runtime#waituntil) that resolves only when the stream ends. Without it, the script can stop in the middle of a stream. Always resolve that promise when the stream closes. A promise that never settles holds the isolate open after the client has gone. ## Caching A stream that a script returns is not cached, and it needs no `Cache-Control` header. The edge serves every request to a standalone script from the script itself. Set `Cache-Control: no-cache` anyway if the same code also runs behind a pull zone as [middleware](/docs/scripting/middleware/overview), where the normal [caching rules](/docs/cdn/server-sent-events#keep-the-stream-out-of-the-cache) apply. ## Limits The [Edge Scripting limits](/docs/scripting/limits) apply, with one point to note: the 30-second CPU time limit measures processor time, and not the time a connection stays open. A stream that waits between events uses almost no CPU time. [Pricing](/docs/scripting/pricing) follows the same rule. Keep the work inside the stream small. A script that builds each event with a subrequest reaches the subrequest limit quickly. ## References * [Server-Sent Events on the CDN](/docs/cdn/server-sent-events) * [WebSockets](./websockets), for two-way communication * [Runtime - waitUntil](./runtime#waituntil) # Fetch url Source: https://bunny.net/docs/scripting/standalone/examples/fetch-url Proxy a request to another URL with fetch, keeping the original path. This script appends the incoming request path to another domain and fetches the content from there. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { const url = new URL(request.url); const fetchUrl = "https://example.com"; return fetch(fetchUrl + url.pathname); }, ); ``` # Route by country Source: https://bunny.net/docs/scripting/standalone/examples/geo-routing Send visitors from specific countries to a different origin using the CDN-RequestCountryCode header. bunny.net adds the [`CDN-RequestCountryCode`](/docs/cdn/vary-cache) header to every request. This script reads it and routes the listed countries to a second origin. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; const PRIMARY = "https://primary.example.com"; const ALTERNATE = "https://alternate.example.com"; // Countries routed to the alternate origin const ALTERNATE_COUNTRIES = new Set(["NZ", "FI"]); BunnySDK.net.http.serve(async (request: Request): Promise => { const country = request.headers.get("CDN-RequestCountryCode") ?? ""; const variant = ALTERNATE_COUNTRIES.has(country) ? ALTERNATE : PRIMARY; const target = new URL(request.url); const origin = new URL(variant); target.protocol = origin.protocol; target.host = origin.host; return fetch(new Request(target, request)); }); ``` Add or remove entries in `ALTERNATE_COUNTRIES` to change which countries get the alternate origin. # Redirect to another domain Source: https://bunny.net/docs/scripting/standalone/examples/redirect-domain Send every request to a different domain with Response.redirect. This script redirects all incoming requests to one URL with a permanent redirect (HTTP status code 301). ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { const destinationURL = "https://example.com"; const statusCode = 301; return Response.redirect(destinationURL, statusCode); }, ); ``` # Return HTML Source: https://bunny.net/docs/scripting/standalone/examples/return-html Build an HTML document in the script and include data from the request, such as the path. This script returns an HTML page and drops the request path into the markup. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { const url = new URL(request.url); const html = ` HTML Edgescript example

Hello

there

Lorem ipsum bla bla
from ${url.pathname}
`; return new Response(html, { headers: { "content-type": "text/html", }, }); }, ); ``` # Return JSON Source: https://bunny.net/docs/scripting/standalone/examples/return-json Respond with JSON, for an API endpoint or for configuration data. This script returns a fixed JSON object with the right `content-type` header. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve( async (request: Request): Response | Promise => { const data = { weather: "sunny", temperature: 27, windspeed: 0, uvindex: 7, }; const json = JSON.stringify(data); return new Response(json, { headers: { "content-type": "application/json", }, }); }, ); ``` # Send an email Source: https://bunny.net/docs/scripting/standalone/examples/send-email Send emails from the edge using popular email providers like SendGrid or Resend. **Resend** To send an email using Resend, use the `resend` SDK. Set your Resend API key as an environment secret named `RESEND_API_KEY`. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; import { Resend } from "https://esm.sh/resend"; import process from "node:process"; const resend = new Resend(process.env.RESEND_API_KEY); BunnySDK.net.http.serve(async (req) => { try { await resend.emails.send({ from: "sender@yourdomain.com", to: "recipient@example.com", subject: "Welcome to Bunny Edge Scripting!", text: "This email was sent directly from the edge.", }); return new Response("Email sent successfully!"); } catch (error) { console.error(error); return new Response("Failed to send email.", { status: 500 }); } }); ``` **SendGrid** To send an email using SendGrid, use the `@sendgrid/mail` SDK. Set your SendGrid API key as an environment secret named `SENDGRID_API_KEY`. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; import sgMail from "https://esm.sh/@sendgrid/mail"; import process from "node:process"; sgMail.setApiKey(process.env.SENDGRID_API_KEY); BunnySDK.net.http.serve(async (req) => { const msg = { to: "recipient@example.com", from: "sender@yourdomain.com", subject: "Welcome to Bunny Edge Scripting!", text: "This email was sent directly from the edge.", }; try { await sgMail.send(msg); return new Response("Email sent successfully!"); } catch (error) { console.error(error); return new Response("Failed to send email.", { status: 500 }); } }); ``` # Weighted traffic splitting Source: https://bunny.net/docs/scripting/standalone/examples/weighted-routing Send a set percentage of visitors to a second origin and keep each visitor on the same origin across requests. This script hashes a stable key (the client IP) into a 0-99 bucket and routes by percentage. The same key always lands in the same bucket, so a visitor stays on one origin across requests. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; const ORIGINS = { a: "https://origin-a.example.com", b: "https://origin-b.example.com", }; const B_PERCENTAGE = 10; // send 10% of visitors to origin B // Polynomial string hash (Java String.hashCode style), mapped into a 0-99 bucket function bucket(key: string): "a" | "b" { let h = 0; for (let i = 0; i < key.length; i++) { h = (h * 31 + key.charCodeAt(i)) >>> 0; } return h % 100 < B_PERCENTAGE ? "b" : "a"; } BunnySDK.net.http.serve(async (request: Request): Promise => { const ip = request.headers.get("x-forwarded-for") ?? "0.0.0.0"; const variant = bucket(ip); const target = new URL(request.url); const origin = new URL(ORIGINS[variant]); target.protocol = origin.protocol; target.host = origin.host; return fetch(new Request(target, request)); }); ``` Adjust `B_PERCENTAGE` to change the split. To pin on something other than the client IP, such as a cookie or a header, hash that value instead. # Standalone scripts Source: https://bunny.net/docs/scripting/standalone/overview Standalone scripts take the place of an origin server and handle HTTP requests directly on the bunny.net CDN network. A standalone script handles the request itself. It serves dynamic content, runs your logic, and shapes the response at the edge, which in many cases means you don't need an origin server at all. Running that close to the user keeps latency down, and a script handles many requests at once. ## Use cases * Serve REST APIs and microservices straight from the edge * Generate dynamic UIs and landing pages based on user interactions or geolocation * Build AI-powered endpoints that process images, run chat completions, or generate embeddings * Back a static frontend with contact forms, webhooks, and data submissions * Call external APIs to send emails, transform data, or integrate with third-party services ## The `serve` function The `serve` function starts an HTTP server that listens for incoming requests and handles them using the provided handler function. ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { return new Response("Hello from the edge!"); }); ``` ### Function signature ```ts theme={null} serve(handler: (request: Request) => Response | Promise) ``` ### Handler The handler function receives a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object and must return a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) or `Promise`. ## Before cache execution Standalone scripts act as the origin for their Pull Zone, so by default they only execute when the cache is missed. If your Pull Zone is configured to execute scripts before cache, your script runs on **every request**, before the cache lookup. > [!NOTE] > > You can enable it in your Pull Zone under **General** > **Origin** > > **Run script before cache**, or directly in the script settings. Learn more > about [before cache execution](/docs/scripting/before-cache). ## Example This example runs an A/B test based on a custom header: ```ts theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (req) => { console.log(`[INFO]: ${req.method} - ${req.url}`); const origin = "bunny.net"; const newOrigin = "dash.bunny.net/auth/register"; // Retrieve scenario header value const scenario = req.headers.get("X-AB"); const url = new URL(req.url); url.port = "443"; url.protocol = "https"; if (scenario === "new") { url.host = newOrigin; } else { url.host = origin; } return fetch(url); }); ``` ### Local development You can run standalone scripts locally using Deno: ```bash theme={null} deno run -A script.ts ``` Test with curl: ```bash theme={null} # Case A - routes to new origin curl http://127.0.0.1:8080/ --header 'X-AB: new' # Case B - routes to default origin curl http://127.0.0.1:8080/ ``` # Statistics Source: https://bunny.net/docs/scripting/statistics See how often your script runs and how much CPU time it uses. The statistics section shows: * The number of times your script has been executed. * The total CPU time consumed by your script, measured in milliseconds. To view the statistics, select the **Statistics** tab in the navigation pane: ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/409fcd3ea94507a251f2dd6550337a941f9457b770a01dde8e7ef4e8567618c9-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=ba58d733fb53ffa5726199d92a6f1239) # WebSockets Source: https://bunny.net/docs/scripting/websockets Use WebSockets in an Edge Script for two-way, low-latency communication between your script and connected clients. ## Enabling WebSockets To use WebSockets in your scripts, you'll first need to [enable it for your Pull Zone](/docs/cdn/websockets#enabling-websockets) in the Dashboard. 1. Navigate to your Pull Zone -> General -> WebSockets 2. Toggle the "WebSockets" switch 3. Call `request.upgradeWebSocket()` in your script to upgrade incoming connections ## Quickstart This example creates a WebSocket server that echoes messages back to the client. ```typescript theme={null} import * as BunnySDK from "@bunny.net/edgescript-sdk"; BunnySDK.net.http.serve(async (request: Request) => { const { response, socket } = request.upgradeWebSocket(); socket.addEventListener("open", () => { console.log("Client connected"); }); socket.addEventListener("message", (event) => { console.log("Received:", event.data); socket.send(`Echo: ${event.data}`); }); const closed = new Promise((resolve) => { socket.addEventListener("close", (event) => { console.log("Client disconnected:", event.code, event.reason); resolve(); }); }); // It is important to call waitUntil; otherwise the script may be evicted // while the WebSocket connection is still open. Bunny.v1.waitUntil(closed); return response; }); ``` ## upgradeWebSocket Upgrades an incoming HTTP request to a WebSocket connection. ```typescript theme={null} const { response, socket } = request.upgradeWebSocket(); // With options const { response, socket } = request.upgradeWebSocket({ protocol: "graphql-ws", idleTimeout: 60, }); ``` ### Parameters **protocol** `string` The WebSocket subprotocol to use for the connection. **idleTimeout** `number` (default: 30) The number of seconds to wait for a pong response before closing the connection. If the client does not respond within this timeout, the connection is deemed unhealthy and closed, emitting the `close` and `error` events. If no data is transmitted from the client for 2 minutes, the connection will be closed regardless of this configuration. ### Returns **response** `Response` *required* The response to send back to the client to establish the upgrade. **socket** `WebSocket` *required* The WebSocket interface to communicate with the client. ## Methods ### close Closes the WebSocket connection, optionally providing a close code and reason. ```typescript theme={null} // Close normally socket.close(); // Close with a code socket.close(1000); // Close with a code and reason socket.close(1000, "Session ended"); ``` #### Parameters **code** `number` A standardized [WebSocket close code](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5). If unset, defaults to `1000` for normal closure or `1001-1015` for error conditions. **reason** `string` A human-readable [close reason](https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6) explaining why the connection was closed. ### send Transmits data to the connected client. ```typescript theme={null} // Send a string socket.send("Hello, client!"); // Send JSON socket.send(JSON.stringify({ type: "message", content: "Hello" })); // Send binary data const buffer = new ArrayBuffer(8); socket.send(buffer); // Send a Blob const blob = new Blob(["Hello"], { type: "text/plain" }); socket.send(blob); ``` #### Parameters **data** `string | ArrayBufferLike | Blob | ArrayBufferView` *required* The data to transmit. Can be a string, ArrayBufferLike, Blob, or ArrayBufferView. ### addEventListener Registers an event listener for WebSocket events. ```typescript theme={null} socket.addEventListener("open", () => { console.log("Connection established"); }); socket.addEventListener("message", (event) => { console.log("Message received:", event.data); }); socket.addEventListener("close", (event) => { console.log("Connection closed:", event.code, event.reason); }); socket.addEventListener("error", (event) => { console.error("WebSocket error occurred"); }); ``` #### Parameters **type** `string` *required* The event type to listen for. One of: `open`, `message`, `close`, or `error`. **listener** `function` *required* The callback function invoked when the event is dispatched. **options** `boolean | AddEventListenerOptions` Optional configuration for the event listener. ## Events ### open Fired when the WebSocket connection is successfully established. ```typescript theme={null} socket.addEventListener("open", (event) => { console.log("Connected to client"); socket.send("Welcome!"); }); ``` **OpenEvent** `Event` A standard Event object with no additional properties. ### message Fired when a message is received from the client. ```typescript theme={null} socket.addEventListener("message", (event) => { const data = event.data; // Handle string messages if (typeof data === "string") { const parsed = JSON.parse(data); console.log("Received:", parsed); } }); ``` **MessageEvent** `object` **properties** **data** `any` *required* The message data sent by the client. **origin** `string` The origin of the message. **lastEventId** `string` The last event ID string. **source** `MessageEventSource | null` The message source. **ports** `ReadonlyArray\` Any transferred ports. ### close Fired when the WebSocket connection is closed. ```typescript theme={null} socket.addEventListener("close", (event) => { if (event.wasClean) { console.log(`Connection closed cleanly: ${event.code} ${event.reason}`); } else { console.log("Connection terminated unexpectedly"); } }); ``` **CloseEvent** `object` **properties** **code** `number` *required* The WebSocket close code provided by the server. **reason** `string` *required* The close reason provided by the server. **wasClean** `boolean` *required* Whether the connection closed cleanly. ### error Fired when an error occurs on the WebSocket connection. ```typescript theme={null} socket.addEventListener("error", (event) => { console.error("WebSocket error occurred"); }); ``` **ErrorEvent** `Event` A standard Event object with no additional properties. ## References * [Server-Sent Events](./server-sent-events), for one-way streaming from the script to the client * [WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455.htm) * [WebSocket mdn Specification](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/) * [Runtime - waitUntil](./runtime#waituntil) # Access Lists Source: https://bunny.net/docs/shield/access-lists Access Lists give you precise control over who can and cannot reach your applications. From blocking malicious proxies and anonymized botnets to allowing trusted infrastructure or internal tools, Access Lists provide real-time traffic filtering that is enforced instantly across our global edge. Access Lists decide which clients can reach your origin based on identifiers like IP, CIDR range, ASN, or country. Curated threat feeds and your own custom rules run together at the edge, so policy changes apply globally without touching your backend. ## Key Features * **Flexible Controls**: Allow, block, challenge, or log traffic based on IPs, CIDRs, ASNs, or entire countries. * **Curated Threat Lists**: Continuously updated feeds from leading reputation sources, available on Advanced, Business, and Enterprise plans. * **Custom Lists**: Build your own access policies, from trusted networks through to high-risk regions. * **Instant Enforcement**: Rules propagate globally within seconds and are applied directly at the edge without adding latency or backend complexity. ## Multi-layer Access Control Access Lists combine curated intelligence with your own rules: * **Curated threat feeds** cover VPNs, Tor nodes, abusive datacenters, and active attack sources. * **Custom rules** allow you to define access logic for IPs, CIDRs, ASNs, or countries. * **ASN-based filtering** gives you broad control over provider networks to stop botnets hiding behind hosting providers. * **Country-level rules** make it easy to allow or block entire regions. ## Access List Modes Each Access List rule gives you full control over how Bunny Shield should handle matched traffic: * **Allow**: Pass trusted traffic through without restriction. * **Block**: Drop malicious requests at the edge instantly. * **Challenge**: Trigger browser verification for suspicious traffic without blocking it outright. * **Log**: Record traffic patterns for analysis without applying enforcement. ## Action Precedence When multiple rules apply to the same request, Bunny Shield follows a defined order of precedence: 1. **Bypass**: Always takes priority. Requests that match a Bypass skip further evaluation from Bunny Shield. 2. **Allow**: Trusted traffic is passed through without restriction. 3. **Block**: Requests are denied immediately if they match a block rule. 4. **Challenge**: Suspicious requests are challenged if no higher-priority action applies. 5. **Log**: If no other action matches, requests can be logged for monitoring and analysis. This order keeps trusted traffic from being blocked and applies security actions consistently across your rules. ## Curated Threat Intelligence Bunny Shield maintains a library of continuously updated reputation feeds that are deployed instantly across the edge. These feeds protect against evolving threats without manual upkeep: * **Advanced**: VPNs, common datacenters, Tor exit nodes, FireHOL Level 1, AbuseIPDB, NetMountains * **Business**: Includes all Advanced feeds plus Blocklist DE, ThreatFox, StopForumSpam, SpamHaus DROP, and more * **Enterprise**: Includes all Advanced and Business feeds, plus support for custom feed integrations ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/78d3b9ba31a96c349979acef3aeedeea5dcafc51a6f188b263399a2433e4d116-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=4a6e8015765588d968c93eef4af066d6) ## Custom Access Lists Custom Access Lists give you complete control over your traffic: * Allow internal ASNs or office IP ranges * Block abusive regions during sensitive launches * Challenge suspicious networks while monitoring behavior * Apply unique lists to different zones or endpoints All custom rules update globally within seconds. ## Logging and Observability Access Lists provide visibility into every decision, so you always know what is being allowed, blocked, or challenged: * **Logged events**: Requests matched by access rules without being blocked * **Actioned events**: Requests that were blocked or challenged * **Curated feed hits**: Requests flagged by built-in reputation lists ![](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/d2ee99d63b88b84e7703df109b6d19ffb3bdeb88a99833fc03843c742c24b5dd-image.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=5d5f06ef4eae378f0049f42e46f3f2d1) ## Configuring via API Access Lists can be managed through the Bunny Shield API. This makes it simple to automate access policies, integrate with existing tools, or apply consistent controls across your infrastructure. # API Guardian Source: https://bunny.net/docs/shield/api-guardian Schema-aware API protection. Bunny Shield enforces your OpenAPI contract at the edge so requests and responses match what your application expects. API Guardian uses your OpenAPI specification as the source of truth for what your API accepts. At the edge, every request is validated against the schema, authenticated against the configured security scheme, and counted against per-endpoint rate limits before reaching your origin. ## Key Features * **Schema-Based Validation**: Upload your OpenAPI specification and turn it into enforceable validation rules that run directly at the edge. * **Full Request Coverage**: Validate path parameters, query strings, headers, cookies, and JSON request bodies against your API contract. * **Edge Authentication Enforcement**: Enforce API keys, Bearer tokens, OAuth2, and OpenID Connect validation before requests reach your origin. * **Response Validation**: Ensure responses match your schema to prevent accidental data exposure or inconsistent output. * **Per-Endpoint Rate Limiting**: Apply granular rate limits aligned with your API structure, including support for dynamic path templates. * **Integrated Protection**: Combine schema validation with targeted WAF-style inspection for injection attacks on selected parameters. ## Schema-driven request validation API Guardian starts with your OpenAPI schema and transforms it into a real-time enforcement layer at the edge. When a specification is uploaded: * The schema is normalized and processed * Internal references are resolved * Validation rules are compiled into native Bunny Shield protections Every incoming request is checked against this contract before reaching your origin. Only requests matching your defined API structure are allowed through. Validation includes: * **Path parameters** to ensure correct structure and types * **Query parameters** to validate allowed fields and formats * **Headers and cookies** to enforce required values * **JSON request bodies** (`application/json` and `*+json`) to match schema definitions ## Handling non-conforming requests API Guardian provides two layers of control over how requests that don’t match your schema are handled. ### Rule execution mode Execution mode controls how API Guardian enforces its decisions globally: * **Log** to observe validation results without blocking traffic * **Block** to enforce validation decisions at the edge This setting applies to all validation outcomes and overrides any individual blocking decision, allowing you to safely test API Guardian in log mode before enforcing it. ### Unmatched path handling Not all requests will match a defined path in your schema. The **Unmatched Path Action** controls how these requests are treated: * **Block** to reject requests targeting undefined endpoints (subject to execution mode) * **Log** to record unmatched requests while still forwarding them * **Ignore** to bypass API Guardian entirely for unmatched paths Only **Ignore** lets an unmatched request reach the Managed WAF ruleset; under **Block** and **Log**, Managed WAF is skipped. Other Shield features such as Rate Limiting and Bot Detection apply regardless of this setting. Requests that **do match** a declared path are always handled by API Guardian’s schema validation and path enforcement, instead of the Managed WAF ruleset, to avoid false positives common with structured API traffic. ### Authentication-aware path handling Unmatched path handling is not limited to literal path definitions. API Guardian automatically allows endpoints referenced by your authentication schemes, so login and token-exchange flows work without manual schema duplication. For OpenID Connect: * The provider’s discovery metadata is fetched * Advertised endpoints (authorization, token, userinfo, JWKS, etc.) are automatically allowed * Keys are kept up to date as they rotate This allows login and token exchange flows to function correctly while still enforcing schema validation across your API. ![](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/shield/api-guardian/request-validation.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=2088fa7fc94bb3fdeca89ab8485874aa) ## Response validation APIs don’t just define what they accept, but also what they return. API Guardian can validate responses against your schema on a per-endpoint basis. This helps prevent: * Unexpected fields being exposed * Incorrect response formats * Edge-case behavior leaking unintended data Responses are validated before leaving your infrastructure, ensuring consistency between your API contract and real-world behavior. ![](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/shield/api-guardian/response-validation.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=52ce6336567e909d1a2d01d10793a733) ## Authentication enforcement at the edge API Guardian enforces authentication requirements defined in your OpenAPI schema before requests reach your origin. Supported authentication methods include: * **API Keys** (headers, query parameters, cookies) * **HTTP Authentication** (Bearer and Basic) * **OAuth2** * **OpenID Connect** Requests missing required credentials are rejected immediately, filtering out a large portion of automated and low-effort attack traffic. ### Token validation For Bearer tokens and OpenID Connect: * **JWT structure and expiration** are validated for `bearerFormat: jwt` * **Signature verification** is performed for OpenID Connect providers using public-key algorithms * **JWKS endpoints** are automatically fetched and refreshed * Tokens must match the expected **issuer** and **signing algorithm** Supported algorithms include RSA and ECDSA (256, 384, 512-bit). Invalid, expired, unsigned, or tampered tokens are rejected at the edge before reaching your application. ![](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/shield/api-guardian/authentication.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=09921c4ec033f3ec7e79f4c4741023ea) ## Per-endpoint rate limiting Not all endpoints behave the same, and API Guardian reflects that. You can define rate limits directly per endpoint: * Supports dynamic paths like `/users/{id}` * Applies limits at the template level across all variations * Configurable per endpoint or per IP address * Time windows from **1 second to 1 hour** Versioned endpoints (e.g., `/v1`, `/v2`) are treated independently, allowing precise control as your API evolves. ![](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/shield/api-guardian/rate-limiting.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=0c192d3a6bd10de36c9d70e5aa0363f0) ## Targeted injection protection Schema validation ensures requests are structurally correct, but some attacks rely on valid structure with malicious content. API Guardian allows you to combine schema enforcement with targeted deep inspection: * Select specific **query**, **path**, **header**, or **cookie** parameters for inspection * Detect patterns like **SQL injection** and **cross-site scripting (XSS)** * Apply deep inspection only where needed, reducing unnecessary overhead ### Schema-level inspection with `x-bunny-shield` You can also define inspection rules directly in your OpenAPI schema using the `x-bunny-shield` extension. This allows you to attach detection logic to specific fields: ```json theme={null} { "properties": { "comment": { "type": "string", "x-bunny-shield": "detectxss,detectsqli" } } } ``` * Applies to `string` schema types * Accepts a comma-separated list of detectors (`detectxss`, `detectsqli`) * Enables field-level inspection without additional dashboard configuration ![](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/shield/api-guardian/injection-detection.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=458a362ee17e5adfe0efdcf6d5461c7d) ## Built into the edge pipeline API Guardian runs natively inside Bunny Shield’s request processing pipeline. * No additional network hops * No external validation services * Minimal latency impact Validation happens alongside WAF, Bot Detection, and Rate Limiting, ensuring invalid traffic is filtered as early as possible. ## Limits and requirements API Guardian includes safeguards to ensure consistent performance: * **Maximum OpenAPI spec size**: 2 MB * Limits on schema complexity, nesting depth, and regex usage * Supports **OpenAPI 3.0.x** specifications Endpoint limits vary by plan: * **Advanced**: up to 10 endpoints * **Business**: up to 50 endpoints * **Enterprise**: customizable These limits help focus protection on critical and high-traffic endpoints. ## Logging & Observability API Guardian provides full visibility into validation activity: * **Validation events** logged for mismatched requests and responses * Clear insight into why a request was flagged or blocked * Integrated into Bunny Shield’s event logs for real-time monitoring This makes it safe to start in **log mode**, observe real traffic patterns, and switch to **blocking mode** once you're happy with the results. ![](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/shield/api-guardian/logs.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=46195fce2cee7dfa0ff8ba51514f6169) ## Configuring via API You can manage API Guardian configurations through the Bunny Shield API. This allows you to: * Automate schema uploads * Manage validation rules programmatically * Integrate API protection into CI/CD workflows By treating your OpenAPI schema as the source of truth, API Guardian enables consistent, automated security across all environments. # API Reference Source: https://bunny.net/docs/shield/api-reference Complete API reference for configuring Bunny Shield security features programmatically. # Bot Detection Source: https://bunny.net/docs/shield/bot-detection Identify and block malicious bots, including headless browsers, impersonators, and scrapers, without affecting legitimate automation or user experience. Bunny Shield identifies automated traffic using behavioral fingerprinting, header anomalies, and IP reputation. Detection rules and sensitivity profiles propagate across the edge within seconds, so policy changes take effect in near real time. ## Key Features * **Behavioral Fingerprinting**: Distinguish real users from automated traffic using fingerprinting, header anomaly detection, and request behavior modeling. * **Real-Time Response**: Detection logic and mitigation rules propagate across the edge within seconds, so policy changes take effect almost immediately. * **Customizable Rules**: Set detection conditions based on HTTP headers, IP reputation, user-agent anomalies, cookie presence, request frequency, and other signals. * **False Positive Resistant**: Allow-listing, ASN validation, and crawl integrity filters keep legitimate crawlers and automation from being blocked. ## Multi-layer request analysis Every request that reaches Bunny Shield is evaluated through multiple layers of analysis, each targeting a different way automation tries to hide: * **Request integrity checks** analyzes headers, query structures, and protocol patterns to detect spoofed or malformed requests. * **Request body inspection** for applicable methods, which will inspect payload structure and behavior to spot signs of scripted abuse. * **External intelligence** uses IP and ASN reputation, rate patterns, and global behavior history to flag known abuse sources. ## Sensitivity profiles You can choose from predefined detection profiles, each tuned to different use cases: * **Low** (*default*) catches basic bots with minimal overhead using lightweight IP and header analysis. * **Medium** applies balanced checks across IPs, headers, and fingerprint signals to detect common automation. * **High** enables strict fingerprint validation, request integrity analysis, and IP behavior scoring to stop advanced or evasive bots. * **Custom** lets you configure individual detection components for total control. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/8e473394675b0cd12cd9e8b427c3f6efe55cdcda921ea57d4245c7c7065abc58-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=372b33768aa3709625c32ed98e307b1b) ## Granular detection toggles With Custom mode enabled, you can adjust: * **Request integrity** looks for anomalies in headers, protocol usage, and request structure. * **IP address** scores requests based on IP reputation, behavior, and known rate patterns. * **Fingerprint sensitivity** determines how assertively Bunny Shield should treat unusual browser fingerprints as bots. * **Complex fingerprinting** (*Business or above*) combines advanced entropy analysis and cross-session consistency. ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/1e86729bb9352d34a81922ae5317d1a9e4fd01c901b6845470b06f8a40beb01c-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=928e88744d0a7ea9145ac65b7a00977c) These options let you tailor detection to match your traffic profile and risk tolerance. And with Edge Rules, you can disable bot detection dynamically based on headers, cookies, IP addresses, or specific endpoints, giving you full control over when and where protection applies. ## Logging & Observability Bot detection isn’t a black box. Bunny Shield shows you exactly what it’s seeing and doing, in real time: * **Logged requests**: Number of requests identified as bots but not challenged. * **Challenged requests**: Number of requests that triggered browser validation. We give you the full picture, with clear metrics and event logs that show what’s being flagged and how it’s being handled. No guesswork required. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/95b722d902c90359a27782b336dbc9d05d189ece6bc37a268f17d849a9092e25-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=3c74ee46001cd143f14a400cc1f78265) ## Configuring via API You can utilize the Bunny Shield API to automate Bot detection configurations or integrate them into your continuous integration and continuous deployment (CI/CD) pipelines. This capability allows you to manage your security settings efficiently and consistently across different environments. # Cookies Source: https://bunny.net/docs/shield/cookies Bunny Shield relies on cookies to support its abuse mitigation features, protect against automated bots, and maintain legitimate user access. These cookies enable the service to validate users efficiently while maintaining security and performance. Bunny Shield sets a small number of short-lived, first-party cookies that back its challenge flow, session validation, and bot detection. ## Cookie Usage Each cookie employed by Bunny Shield serves the essential purpose of ensuring that legitimate users can interact with websites protected by this service. At the same time, these cookies are instrumental in preventing abuse and minimizing the impact of bot traffic or other malicious automated behavior. As these cookies focus exclusively on verifying the authenticity of user interactions, they contribute to a secure and trusted browsing experience without overstepping into the collection of unnecessary or personal data. ## Privacy and Compliance All the cookies listed above are strictly necessary to deliver the services provided by bunny.net to our customers unless otherwise specified. bunny.net encourages customers to disclose the use of these cookies to their end users. Depending on regional legal requirements, you may need to inform your users about these cookies and the reasons behind their presence, in line with local regulations. * These cookies never track user behavior beyond verifying the browser request. They contain no personally identifiable information and are short-lived, first-party cookies used only for verifying legitimate access. ## Cookies `bunny_shield` * A temporary indicator that the user has completed the JavaScript Proof-of-Work (PoW) challenge. Once set, it lets Bunny Shield recognize validated users without re-running the challenge on every visit. `bunny_shield_chk` * A checksum or validation token that verifies the integrity of the `bunny_shield` cookie. This prevents tampering, forgery, or unauthorized changes to the validation data. `bunny_shield_id` * A persistent, first-party identifier used by Bunny Shield’s bot detection. It assigns a unique ID to each browser session so the system can distinguish legitimate users from malicious actors over time. No personally identifiable information is collected. `bunny_shield_bd` * Captures a non-invasive fingerprint of the user's browser environment, including rendering behavior, device attributes, and feature support. The fingerprint helps Bunny Shield identify automation patterns and spoofed or emulated clients while preserving user anonymity. # Creating a rate limit rule Source: https://bunny.net/docs/shield/custom-rate-limit-rule Customize Bunny Shield’s protections to throttle abuse, slow malicious bots, and absorb traffic spikes without affecting legitimate users. Rate limit rules cap how many requests a client can make to your application within a defined window. Combine them with Shield's targeting controls to throttle abuse on specific endpoints without affecting legitimate traffic. ## What you'll need Before you dive in, make sure you have the following prerequisites in place: * A [bunny.net](https://bunny.net/) account ([Log in](https://dash.bunny.net/auth/login?pk_buttonlocation=menu) or sign up for a [free trial](https://dash.bunny.net/auth/register)). * An existing Shield Zone. * Advanced Plan or above on the existing Shield Zone (if creating more than 2 rate limit rules\*). ## Creating a rate limit rule To create an effective rate limit rule, it's important to understand the fundamentals of how to build a Custom WAF Rule on Bunny Shield. Familiarize yourself with our [Rule Engine](/docs/shield/rule-engine) documentation to gain insights into how rules are structured and processed within the system. ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/159a0ca2d03fb26291cd1443372d4806e88716d14bbf377db6bad5984979377e-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=8bdd4687ebfec3fca9db9acab6216fd0) This rule processes each HTTP request by extracting only the REQUEST\_URI (**Variable**), converting it to lowercase, and removing whitespaces (**Transformations**). It then verifies if the transformed REQUEST\_URI matches exactly (**Operator**) with '/blockedpath’ (**Operator Value**). If a match is found, we increment the global rate limit counter. If the RequestCount (10 requests) is exceeded within the defined 1-second Timeframe, then our WAF Engine will block (**Response Action**) the request for the defined 30-second BlockTime, halting further rule processing and intercepting the request. With the basics covered, you can write rate limit rules that mitigate the abuse patterns specific to your site. ## Examples ### Rate limit request if a cookie is set with a specific value and exceeds a defined limit If you want to rate limit requests when a specific cookie is set to a certain value and the request rate exceeds a defined limit, you can use the following configuration: ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/2fcc5381951d8095a887ce2a0479ffcde7c7fd808fa9a9f96994f772e664d8eb-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=4203ceafb83504711b88e9fb67b52e5b) ### Rate limit request if User-Agent is a known crawler and exceeds a defined limit If you need to rate limit requests where the User-Agent header contains a known crawler identifier and the request rate exceeds a defined limit, use the following rule configuration: ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/3c473840055c08fe347e4bbf83ef7771cad56a666b33ef65482abcce35dc41c4-awdawdawdawwdawdawdawdawd.pg.jpg?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=b69c0ff05fbcc046e2b20c03f8977c86) ## Need help or encountering issues? If you encounter any difficulties or have questions, our support team is here to assist you. Please don't hesitate to contact us via [support request form](https://bunny.net/contact/) for prompt assistance. Our dedicated support team is ready to help you resolve any issues you might face during the deployment process, provide additional guidance, or answer your questions. # Custom Response Pages Source: https://bunny.net/docs/shield/custom-response-pages Customize the challenge, block, and rate limit response pages shown to visitors when Bunny Shield takes action. Create fully branded experiences that match your website while maintaining Bunny Shield protection. Custom Response Pages allow you to fully customize the content displayed to visitors when Bunny Shield issues a challenge, blocks a request, or enforces a rate limit. Instead of generic protection pages, you can present visitors with branded experiences that match the look and feel of your website, helping maintain a consistent user experience even during security events. Custom Response Pages are available on **Advanced** and higher Bunny Shield plans. ## Overview By default, Bunny Shield serves clean, generic response pages whenever a request is challenged, blocked, or rate limited. With Custom Response Pages enabled, you can replace the contents of the page body with your own HTML, allowing you to: * Match your website branding and styling * Provide additional support or contact information * Explain why access was restricted * Offer troubleshooting instructions for legitimate visitors * Create localized or multilingual response pages The response status codes and Bunny Shield protection logic remain unchanged. Only the page content presented to visitors is customized. ### Supported Response Types You can customize the following Bunny Shield response pages: * **Challenge Pages** * Displayed when visitors must complete a verification challenge. * **Block Pages** * Displayed when Bunny Shield blocks a request due to security policies. * **Rate Limit Pages** * Displayed when a visitor exceeds configured rate limits. ## How It Works Bunny Shield provides a customizable HTML editor for each response page type. For security and reliability reasons, only the contents within the page's `` element can be customized. The editor already wraps your code in a `` element, so you only need to provide the content that goes inside it. Within the body, you can include your own HTML, CSS, JavaScript, and external assets to fully customize the visitor experience. ```html theme={null}
Company Logo

Security Verification Required

Please complete the verification below to continue.

``` The surrounding document structure, response handling, challenge functionality, and security controls remain managed by Bunny Shield to ensure the page continues to function correctly, while giving you complete control over the visitor-facing content and styling. ## Configuring Custom Response Pages To configure custom response pages: 1. Open your **Shield Zone**. 2. Navigate to **Settings** -> **Response Pages**. 3. Enable **White-label block pages**. 4. Click **Create custom page** for the response you'd like to customize. 5. Edit the HTML body content. 6. Save your changes. ### Response Page Configuration ![](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/shield/custom-response-pages/response-pages.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=e422046bc6e4df7e3f1486c3836a23fd) ### HTML Editor ![](https://mintcdn.com/bunnynet-cb9733c2/54-4KCQKoNVNuoyW/images/shield/custom-response-pages/custom-response-page-editor.png?fit=max\&auto=format\&n=54-4KCQKoNVNuoyW\&q=85\&s=6115ddcf2c3cb317a3d7df4ae58e3b0b) ## Example Use Cases ### Branded Challenge Page Add your company logo, colors, and messaging to create a seamless verification experience. ```html theme={null}
Company Logo

Security Verification Required

Please complete the verification below to continue.

``` ### Custom Block Page Provide additional information for visitors who may have been blocked unintentionally. ```html theme={null}

Access Restricted

Your request was blocked by our security systems. If you believe this is an error, please contact support.

``` ### Custom Rate Limit Page Help users understand temporary access restrictions. ```html theme={null}

Too Many Requests

You have exceeded the allowed request rate. Please wait a few minutes and try again.

``` ## Fallback Behavior If a custom response page is not configured, Bunny Shield automatically serves a generic, non-branded response page. These default pages provide a clean and professional experience without displaying Bunny.net branding. ## Best Practices When creating custom response pages, we recommend: * Keeping messaging clear and concise. * Explaining why the visitor is seeing the page. * Providing support or contact information where appropriate. * Ensuring branding matches your website. * Avoiding excessive scripts or external dependencies. * Testing pages on desktop and mobile devices. ## Frequently Asked Questions ### Can I modify the entire HTML document? No. Bunny Shield only allows customization of the content within the page's `` element. This includes any HTML, CSS, and JavaScript you wish to add, including ` ``` New (Bunny Stream Player): ```html theme={null} ``` ### 2) Rounded player and poster styling Legacy (Plyr): ```html theme={null} ``` New (Bunny Stream Player): ```html theme={null} ``` ### 3) Hide specific controls (PiP and seek) Legacy (Plyr): ```html theme={null} ``` New (Bunny Stream Player): ```html theme={null} ``` ### 4) Captions styling Legacy (Plyr): ```html theme={null} ``` New (Bunny Stream Player): ```html theme={null} ``` ## **Migration checklist** 1. Search your custom snippet for `plyr`, `.plyr__`, and `data-plyr`. 2. Replace selectors using the mapping table. 3. Search for jQuery usage `($(, jQuery) `and rewrite to vanilla JavaScript. 4. Verify on desktop and mobile, especially controls and captions.   Migrating from Plyr-specific customizations to media-chrome element-based styling gives you a cleaner and more future-friendly setup. For the best results during migration: * Use your browser’s standard developer tools (Inspect Element, computed styles, and DOM viewer) to explore the current player structure. * Use stable element selectors and CSS variables over deep internal hooks. * Validate each customization incrementally so you can quickly isolate issues. # Dashboard Source: https://bunny.net/docs/stream/dashboard The Bunny Stream dashboard is divided into several key areas, each dedicated to a specific aspect of video content management and delivery. The dashboard is the easiest way to upload videos, configure your library, customise the player, and monitor delivery. The sections below walk through each area you'll find under Stream > Your Library. ## Manage library The foundation of the dashboard, this section enables the uploading, organization, and management of video content. You can add new videos, create collections for better organization, and manage existing content with ease. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/5c70af6-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=321a7ee82e9b2a2409f934843fdd2af4) **Key Features:** * **Uploading and Managing Videos** - You can upload Videos directly to the platform, allowing for immediate management and organization. Here you can also create collections to categorize content, facilitating easier access and navigation. * **Content Updates and Modifications**- You can update or remove content as necessary, ensuring the video library remains current and relevant. ## Player section This area focuses on tailoring the video player's look and functionality to match your branding or specific preferences. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/8613221-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=6b81fe6b7ce9f319809a43ee6d92ef5e) ![](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/d7d1a560564688474a361d548acafbf00eca1bdee89594f9446697b72057c059-Screenshot_2025-06-27_at_14.35.23.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=a575111432ab9c691d087e9aca776261) **Key Features:** * **Visual and Functional Customization** - Options include adjustments to the player's appearance, such as colors and fonts, and functionality, like autoplay settings and looping. * **Playback Speed** - Set custom playback speed options within your video player, giving you flexibility for custom default playback speeds and additional options. * **Brand Integration** - You can add your logo and incorporate branding elements directly into the player, promoting brand consistency and recognition. * **Watchtime Heatmap** - Enable the watchtime heatmap within your player so viewers can view the most popular parts of your video from the transport bar. * **Resumable Player** - Enable your video player to remember and resume playback where the video was last left when viewers leave and return to your video experience. ## Advertising The Advertising section allows you to monetize your content by displaying VAST-compatible ads within your videos. ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/22d996f-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=7e8b2291fe69479cf58c23f572bc3b7f) **Key Features:** * **Ad Setup and Configuration** - You can easily embed advertisements, customize their appearance and timing, and adjust settings to fit your monetization strategy. ## Encoding options In the Encoding section, you can manage the process of converting your uploaded videos into different formats and resolutions to ensure optimal playback across devices and bandwidths. ![](https://mintcdn.com/bunnynet-cb9733c2/kcU5tb-Sz5318NF5/images/docs/ed23c736642f32cb58564bd982b0690d93b903d3d5e49c1a5f838929f7a604ab-image.png?fit=max\&auto=format\&n=kcU5tb-Sz5318NF5\&q=85\&s=15e66c41de06013835a39b066a496350) **Key Features:** * **Automated Encoding and Customization** - Automatic conversion of uploaded videos is supported, along with manual settings for resolution and bitrate adjustments. * **Watermark feature** - You can apply watermarks to your videos for branding purposes. * **Keep Original Files** - This is the original file of your video, which is useful if you want to store your original files with us as well, along with the encoded ones. * **Enable Early-Play** - Enabling Early-Play can come in handy if you wish to play your video before all the resolutions are done transcoding. Using this option will use the resolution that your original video comes with and will play the original file, which in turn, will expose the original file to the public. Do not toggle this on if you do not wish the original file to be accessible. * **Enable Content Tagging** - Content tagging is still a preview at this point and will only tag videos at the moment with their associated content. ## Transcribing and accessibility The Transcribing feature offers a suite of tools for automatically generating and managing subtitles and captions. This functionality not only improves accessibility for a wider audience but also boosts viewer engagement by providing a textual representation of the audio content. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/92d7993-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=075fb1dc583f1508d44b3d10f3b503db) **Key Features:** * **Automatic Transcription** - Automatically generates subtitles and captions for videos, eliminating the need for manual transcription efforts. * **Transcription Translation** - Offers the capability to automatically translate the generated subtitles and captions into multiple languages. ## Security features Ensure the protection of your video content through the Security section, which encompasses a range of features designed to control access and secure your videos. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/7977cbb-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=b757a228989b10f96308436b68ae1196) **Key Features:** * **Token Authentication** - Use token-based authentication to secure your videos. This method ensures that only viewers with a valid token can access your content, providing an additional layer of security. * **Domain Restriction** - Control where your video content can be played by specifying allowed domains. This feature prevents your videos from being embedded or played on unauthorized websites. * **Block Direct URL File Access** -You can enable this setting to prevent direct access to your video files without a proper referer header. This measure blocks attempts to access and download your content directly via its URL, offering an additional layer of protection against unauthorized distribution. > [!NOTE] > > These settings combine. Enabling **Block Direct URL File Access** without populating **Allowed domains** will return a 403 on thumbnails, previews, and direct file URLs even when the embed iframe still works. Each library's security settings are independent — duplicating a library does not copy its security config. See [Security Options](/docs/stream/security-options) for the full breakdown. ## Delivery optimization The Delivery section of the Bunny Stream Dashboard is designed to help you manage how your video content is distributed and accessed worldwide, ensuring optimal performance and cost-efficiency. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/be7ad1e-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=f1c496133f2e9dd37fcbfe6997643f64) **Tier Selection** **Standard Tier**: This option is tailored for smaller websites where performance is paramount. It's designed to ensure every millisecond counts, making it ideal for scenarios where high-speed content delivery is critical. **High Volume Tier**: Suited for delivering large files, this tier is targeted at users who require a cost-effective solution for global high-bandwidth distribution. It offers the best price-to-performance ratio, making it an optimal choice for extensive video libraries needing wide-reaching access. **Key Features**: * **Routing Filters** - Routing Filters provide a means to precisely control the distribution pathway for your video content across Bunny.net's Content Delivery Network (CDN). ## Storage management In the Storage section you can manage your video storage, keeping track of your usage and optimizing storage allocation. ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/51e7fc3-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=b1f7fa55c89f32d20ab415d655bcff2f) **Key Features:** * **Efficient Use of Resources** - You can manage the storage space and implement the geo-replication to ensure content is stored and accessed effectively. ## Performance statistics The Statistics section offers a comprehensive suite of analytics tools designed to provide insights into the performance of your video content. It allows you to measure viewer engagement and the overall reach of your content, allowing for informed decisions to enhance your video strategy. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/b9e8a0a-image.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=191e1d8a90dd6b8b25a134781d10c229) **Viewer Analytics** * **Views Tracking** - Monitor the number of views your videos receive to gauge their popularity and reach. * **Watch Time** - Assess the total time viewers spend watching your videos, providing insights into engagement levels. * **Viewer Demographics**-  Understand your audience by analyzing demographic data, such geographic location.  **Performance Metrics** * **Video Delivery Performance**- Evaluate the efficiency and speed of your video content delivery. This metric helps identify potential bottlenecks or issues affecting viewer experience. * **Optimization Opportunities**- Analyze performance data to uncover opportunities for improving content delivery and optimizing viewer engagement. This could involve adjustments to video quality, encoding settings, or content distribution strategies. ## API and webhook integration The API section is designed to provide you the access to the Bunny Stream API, facilitating the integration and automation of video streaming functionalities within applications. **Key Features:** * **Access Credentials**- Here you can find information about your Video library ID, CDN Hostname, Pull Zone, and Api Key. * **Webhook URL**- You can set up a Webhook URL to receive automatic callback notifications. *** [Tutorials](/docs/stream) [Upload video from URL](/docs/stream/url-fetch) * [Table of Contents](#) # Data and Privacy Source: https://bunny.net/docs/stream/data-and-privacy At bunny.net, we take data privacy seriously. We’re committed to being fully transparent about the information we collect, how we use it, and how we keep it secure. ## Bunny Stream & Bunny Player Bunny Stream and Bunny Player are designed to avoid collecting or permanently storing user-identifying information for analytics or playback functionality. The player does not set cookies or transmit personally identifiable information (PII) as part of its playback or analytics features. To ensure optimal performance and reliability, the player may send anonymised technical telemetry, such as stream quality metrics, error reports, and buffering statistics. In addition, the embedded player (iframe) may communicate with bunny.net CDN endpoints worldwide to collect anonymous performance statistics. This data is used exclusively to improve routing decisions and automatically optimise streaming performance. ## Resumable Playback Resumable playback is managed entirely within the user's browser using Browser Local Storage on the user's device. No playback information is shared with or stored by bunny.net . Stored playback data is retained locally for up to seven (7) days, after which it is automatically deleted. The playback position is updated every second during viewing and is also saved when the user closes the browser tab or the page is unloaded. The playback position is not saved if the video is within three (3) seconds of completion. This design allows users to resume playback while ensuring that all playback data remains stored exclusively on the user's device. Playback data is never transmitted to or stored by bunny.net . Any technical telemetry transmitted by the player is anonymized and used solely to monitor service health and optimize streaming performance. It is never used for user tracking or identification. > [!NOTE] > > For more information check out our [GDPR](https://bunny.net/gdpr/) and [Privacy & Data](https://bunny.net/privacy/) policies. # Delivery Tiers Source: https://bunny.net/docs/stream/delivery-tiers From a configuration and technical perspective, there is practically no difference between Standard and Volume zones. Where Standard and Volume tiers do differ, is the network performance and the amount of PoPs used when routing your users. Bunny Stream's delivery is split into two tiers — Standard and Volume — that differ only in the routing network used to reach viewers. Pick the tier that matches your latency and cost requirements. ## Standard Tier Standard tier offers exceptionally low global latency aimed at high-performance solutions where every millisecond matters, such as website acceleration or ad delivery. The Standard tier gives you access to the full Bunny.net network and is one of the best performing networks around the globe. We bill this tier based on the region you serve traffic in, please see the diagram below: ![](https://mintcdn.com/bunnynet-cb9733c2/GYo51EPI6zuebYhU/images/docs/195281d72a5ab2fd08eba3b56468ba52ecdfd16fdcfb482e2881c6b40eb06102-image.png?fit=max\&auto=format\&n=GYo51EPI6zuebYhU\&q=85\&s=a67ba8f9d439eb88b5514c559b525a5b) ## Volume Tier The Volume tier uses a smaller number of PoPs, that were carefully selected and optimized to offer a good performance at a fraction of the cost. The Volume tier is perfect for large file delivery, such as video, software delivery or other downloads. We are constantly monitoring the network for performance and make adjustments as needed in order to provide the best possible speeds on the Volume tier. Please see our [Network](https://bunny.net/network) page for a full listing of both of our Standard and Volume PoPs. We offer a flat rate of \$5 per TB served on this tier. # MediaCage DRM Source: https://bunny.net/docs/stream/drm/index MediaCage DRM (Digital Rights Management) solutions are designed to safeguard your digital content and protect it from unauthorized access and distribution. Bunny Stream offers two distinct media content protection tiers: **MediaCage Basic DRM** and **MediaCage Enterprise DRM**. Each product is tailored to meet different security needs, providing you with the flexibility to choose the level of protection that aligns with your specific requirements. | Feature | MediaCage Basic DRM | MediaCage Enterprise DRM | | - | - | - | | Browser support | ✅ | ✅ | | Native platform support | | ✅ | | Clear key encryption | ✅ | | | Secure key management | | ✅ | | Download protection | ✅ | ✅ | | Screen grab protection | | ✅ | | Custom player support | | ✅ | \**Screen Grab Protection is not available on Widevine clients with L3 level of security. For a deeper understanding, refer to [Widevine DRM Security Levels](/docs/stream/widevine-security-levels).* ## MediaCage Basic DRM **MediaCage Basic DRM** is a basic protection solution and it serves as an entry-level digital content protection solution. It employs innovative dynamic encryption, ensuring that your content is encrypted on demand. This encryption process occurs internally, securing your content as it traverses the network to reach your users. ### Key features * **Dynamic encryption**: Utilizes an innovative dynamic clear key encryption for on-demand content protection. * **Download prevention**: MediaCage Basic DRM employs a special mechanism to prevent download of media files. * **Session-based content key protection**: The content is encrypted with a unique key for each play session. **MediaCage Basic DRM** is suitable for scenarios where basic content protection is required. It is an innovative solution for users seeking entry-level security measures for their digital content. For premium, industry or pay-per view video applications and platforms we recommend Mediacage Enterprise DRM for full protection. ## MediaCage Enterprise DRM **MediaCage Enterprise DRM** is an advanced protection feature which represents our advanced digital content protection solution, offering an elevated level of security for enterprise-level requirements. It incorporates underlying industry standard technologies ([FairPlay Streaming](https://developer.apple.com/streaming/fps/) and [Widevine](https://www.widevine.com/)), providing enhanced encryption and protection features. ![](https://mintcdn.com/bunnynet-cb9733c2/L57MhyfuZyBVCcmn/images/docs/6f3c031-DRM_enterprise_.png?fit=max\&auto=format\&n=L57MhyfuZyBVCcmn\&q=85\&s=d61f22e7efa38f9802ef81426e1e279e) MediaCage Enterprise DRM workflow **Enterprise Media Cage DRM** is designed to meet the stringent security needs of organizations that demand the highest level of content protection. In comparison to our MediaCage Basic DRM, the MediaCage Enterprise DRM protects the content also on client devices using hardware vendor support for protecting content key during the playback. ### Key features * **Automatic encryption during packaging**: Content is automatically protected during the transcoding process. * **Advanced encryption technologies**: Utilizes FairPlay (Apple) and Widevine (Google) for superior content encryption. * **Hardware-based key exchange**: Ensures secure content key is not exposed on client devices. * **Screen grab protection**: Prevents screenshots and screen recordings, so that content cannot be copied. Bear in mind that Screen Grab Protection is not available on Widevine clients with L3 level of security. For a deeper understanding, refer to [Widevine DRM Security Levels](/docs/stream/widevine-security-levels). * **Premium content licensing**: Ensures that a valid play license is issued only to authenticated clients that meet protection level requirements. * **Adherence to industry standards**: Meets advanced industry standards for digital content protection. Tailored for Enterprise Security: Specifically designed to address the security needs of enterprise-level users. * **Multi-key DRM**: Multi-key DRM licensing is implemented on a per-playback-device basis, allowing multiple licenses to be issued for a single piece of video content. For example, a single playback device may receive multiple separate licenses. One for the video stream, 2 for the accompanying translated stereo audio tracks and these licenses may then renew due to total video duration; resulting in 6 DRM licenses being issued and billed for one video playback. **Media Cage Enterprise DRM** is the ideal choice for organizations with high-security demands, seeking advanced protection mechanisms and adherence to industry standards. It is suitable for scenarios where content integrity is of utmost importance, and enterprise-level security is a priority. **Supported browsers for Widevine and Fairplay:** | Browser | Widevine | FairPlay Streaming | | - | - | - | | Microsoft Edge | ✅ | | | Chrome | ✅ | | | Firefox | ✅ | | | Safari | | ✅ | | Android | ✅ | | | iOS | | ✅ | ### FairPlay streaming **FairPlay streaming**, a technology developed by Apple, is an underlying component of our MediaCage Enterprise DRM solution. It provides a highly secure mechanism for content protection on Apple devices. This hardware-centric approach significantly enhances the overall security of your content, limiting key exchanges to Apple devices, including Macs, iPhones, iPads, and Apple TV. > [!NOTE] > > To learn more about FairPlay streaming, see the official [FPS website](https://developer.apple.com/streaming/fps/). To use FairPlay Streaming to deliver your content you have to request the FPS Deployment Package from Apple. You can get more information about the process in our FairPlay Streaming Certificate Registration guide. ### Widevine Widevine provides digital media solutions for the delivery of digital entertainment to a wide range of device platforms. Widevine is integrated into our MediaCage Enterprise DRM solution. The technology is built-in a number of device platforms such as Windows, Android, ChromeOS and browsers such as Chrome, Firefox, Edge... > [!NOTE] > > To learn more about Widevine technology, see the [official Widevine documentation portal](https://developers.google.com/widevine). ### Pricing #### One simple plan: \$99/month plus DRM-license fees Our unique pricing model consists of a base fee of \$99 per month, coupled with additional costs for DRM-license fees. This approach allows you to pay only for what you use, ensuring that your costs are always aligned with your actual consumption of DRM licenses. This means you only pay for the licenses you use, in addition to the monthly base fee. We employ a tiered pricing strategy for DRM licenses to provide you with the most cost-efficient solution possible. The cost per license decreases as your volume of licenses increases, as detailed below: | Licenses/Month | Cost per license | | - | - | | Up to 20,000: | \$0.005 | | 20,001 to 100,000: | \$0.004 per license | | 100,001 to 500,000: | \$0.003 per license | | Over 500,000: | Please contact us for custom pricing | This usage-based model allows for significant flexibility and control over your costs, making it easier to budget and plan for your DRM needs. > [!NOTE] > > Enterprise DRM ensures that videos are encrypted during the transcoding process and stored securely in the storage. > > **Multi-key DRM** For example, a single playback device may receive multiple separate licenses. One for the video stream, 2 for the accompanying translated stereo audio tracks and these licenses may expire and issue new licenses due to the total video duration; resulting in 6 DRM licenses being issued and billed for one video playback. #### What counts as a DRM-license? A DRM license is issued each time a user initiates playback of a video that requires a DRM (Digital Rights Management) license for decryption. Our system is designed to count licenses in a manner that ensures fairness and cost-effectiveness: * Multi-key DRM licensing is implemented on a per-playback-device basis, allowing multiple licenses to be issued for a single piece of video content per playback device. The video and audio tracks can be issued a separate license. * Each unique VoD (Video on Demand) content playback on a device counts as one DRM license. * SD videos or audio only streams do not incur DRM license fees. **Examples:** * **Multiple browser playback**: If a user plays back a video in HD and then restarts the VoD session in a different browser, that could count as 4 licenses. * **Playlist viewing**: Watching a playlist with 5 HD VoD videos results in 5 DRM licenses, one for each video. * **Resolution upgrade**: A video played in UHD typically incurs 2 DRM license requests if the player initially starts in SD or HD before switching to UHD. * **SD resolution**: Watching a video in SD resolution does not count towards DRM license usage. #### Monthly cost calculation example To illustrate, if 10,000 users each watch 15 HD videos in a month, this results in 150,000 DRM licenses. Applying the tiered pricing model, the cost for 150,000 licenses amounts to \$669. **Example:** (20,000 x 0.005) + (80,000 x 0.004) + (50,000 x 0.003) = 100 + 320 + 150 = \$570 + \$99 base fee = **\$669** > [!NOTE] > > Customizing or modifying the Bunny Player does not weaken core DRM protection. Encryption, key exchange, and download prevention are enforced by our back-end and the underlying DRM (Widevine/FairPlay) regardless of the player used. > > **Screen capture (screenshots/screen recording) cannot be fully prevented on lower-security devices** e.g. Widevine L3. As is standard across the industry, DRM limits these devices to a lower resolution (480p), so any capture is capped at SD quality if set by the User. Available security levels are determined by the client and User, which is outside of Bunny's control. See [Widevine DRM Security Levels](/docs/stream/widevine-security-levels). # Embedding videos Source: https://bunny.net/docs/stream/embedding Bunny Stream is designed for developers and content creators to easily upload, process, and display videos within any application or website. Once your videos have been processed, you can embed them by using our lightweight embed player, enabling seamless playback across all devices and browsers. Once a video has finished processing, you can embed it on any page using the Bunny Stream player. This page covers the iframe URL pattern, sizing, and player parameters. ## How to embed a video Place an ` ``` ## Sizing and aspect ratio The Stream player is responsive and automatically fills the width and height of its iframe. You can style the iframe like any other HTML element to match your video and the requirements of your site. ### Portrait video Portrait (9:16) video needs no special player configuration. Set the iframe's aspect ratio to match the video and the player fills it: ```html theme={null} ``` Change `aspect-ratio` to match your video, such as `16 / 9` for landscape or `1 / 1` for square video. Constrain the container width to suit your layout. `aspect-ratio` is supported by all current browsers. If you need to support older browsers, size a wrapper with padding instead and stretch the iframe inside it: ```html theme={null}
``` The padding percentage is height divided by width: `56.25%` for 16:9, `177.78%` for 9:16. ## Embedding restrictions Settings outside the embed code can stop the player from loading. Check them when an embed shows a 403 error. **Allowed and blocked domains.** The library security settings can restrict playback to specific domains. The player identifies the embedding site from the `Referer` header of the iframe request, and matches its hostname against the lists. See [Security options](/docs/stream/security-options#domains) for how entries are matched. **Referrer policy.** Browsers send the page origin to a cross-origin iframe by default, which is all the domain check needs. If your site sets a stricter policy, such as `Referrer-Policy: no-referrer` or `same-origin`, the iframe request arrives without a referrer and is treated as direct access. It is then blocked when **Block Direct URL File Access** is enabled. Override the policy on the iframe itself: ```html theme={null} ``` [Embedding videos](/docs/stream/embedding) covers sizing, per-embed parameters such as `autoplay` and `captions`, the direct play URL for sharing a link, and the security settings that can block an embed. ## What the player does * Adaptive HLS playback with a quality menu, and optional native HLS in Safari. * Captions, including auto-generated transcription tracks, and an audio track menu for videos with several languages. * Chapters and moments on the progress bar, with thumbnail previews on hover. * Playback speed control, a watch-time heatmap, and a resume-where-you-left-off option, each configurable per library. * Picture-in-picture, fullscreen, Chromecast, and AirPlay. * [Keyboard shortcuts](/docs/stream/embedding#player-keyboard-shortcuts) for playback, seeking, volume, captions, and speed. * An interface translated into 27 languages besides English, selected with the `lang` parameter. * [MediaCage DRM](/docs/stream/drm) and [VAST ads](/docs/stream/vast). Each playback session feeds the [statistics](/docs/stream/statistics) in your dashboard. ## Customise the look and defaults [Player settings](/docs/stream/player-settings) in the dashboard set the colours, logo, controls, and default behaviour for every embed in a library. Query parameters on the embed URL override those defaults for a single embed. ## Control playback from your page The player speaks the [player.js](https://github.com/embedly/player.js) protocol, so your page can play, pause, seek, mute, and listen for events such as `timeupdate` and `ended`. [Playback control API](/docs/stream/playback-api) lists every method and event, and the framework guides show a working component for each stack: > **[React](/docs/stream/player/react)** > **[Next.js](/docs/stream/player/nextjs)** > **[Vue and Nuxt](/docs/stream/player/vue)** > **[Svelte and SvelteKit](/docs/stream/player/svelte)** > **[Astro](/docs/stream/player/astro)** > **[JavaScript](/docs/stream/player/javascript)** Each guide has a runnable app in [BunnyWay/examples](https://github.com/BunnyWay/examples/tree/main/stream) that plays a demo video straight after cloning. ## Native apps For iOS and Android apps, the [iOS SDK](/docs/stream/ios-sdk) and [Android SDK](/docs/stream/android-sdk) play the same HLS output through the platform's native video stack. To play your library through Bitmovin, Shaka, or another web player, see [Third-party players](/docs/stream/players). ## Legacy player Libraries created before Bunny Player launched may still run the previous Plyr-based player at `iframe.mediadelivery.net`. Embed URLs on that host keep working while the legacy player is enabled for the library. > [!NOTE] > > The legacy player is deprecated and will be removed in early 2027. Move your embeds to `player.mediadelivery.net` before then. If you customised it with custom head HTML, the [migration guide](/docs/stream/custom-head-html-migration-guide) maps the old selectors to the new ones. To switch a library between players in the dashboard, open **Stream**, select the library, open **Player Settings**, and toggle **Enable legacy player**. ![Enable legacy player toggle in Player Settings](https://mintcdn.com/bunnynet-cb9733c2/1bTPx1eyQm8EK8W3/images/Screenshot2026-03-12at10.52.17.png?fit=max\&auto=format\&n=1bTPx1eyQm8EK8W3\&q=85\&s=0af713d294ed72f899e8d720632291aa) The same switch is `PlayerVersion` on the [Update Video Library](/docs/api-reference/core/stream-video-library/update-video-library) endpoint, where `2` is Bunny Player and `1` is the legacy player: ```bash theme={null} curl https://api.bunny.net/videolibrary/ \ -X POST \ -H 'authorization: ' \ -H 'content-type: application/json' \ -d '{"PlayerVersion": 2}' ``` Changing the player does not rewrite embed codes you have already published. Update those URLs to `player.mediadelivery.net/embed/` yourself. # Overview Source: https://bunny.net/docs/stream/player-settings This area focuses on tailoring the Bunny Player's look and functionality to match your branding or specific preferences. **Key Features:** * **Visual and Functional Customization** - Options include adjustments to the player's appearance, such as colors and fonts, and functionality, like autoplay settings and looping. * **Playback Speed** - Set custom playback speed options within your video player, giving you flexibility for custom default playback speeds and additional options. * **Brand Integration** - You can add your logo and incorporate branding elements directly into the player, promoting brand consistency and recognition. * **Watchtime Heatmap** - Enable the watchtime heatmap within your player so viewers can view the most popular parts of your video from the transport bar. * **Resumable Player** - Enable your video player to remember and resume playback where the video was last left when viewers leave and return to your video experience. * **Compact Controls** - Maximize screen real estate with a streamlined video player view, ideal for mobile and embedded players. * **Chromecast** - Cast videos to Chromecast-enabled devices, with a custom cast experience that syncs player colors and captions styling, supports audio track selection and playback speed control, and works with eDRM-protected content. * **AirPlay Support** - Stream videos to Apple TV via AirPlay using the Bunny Player. ## Update the player from the CLI [`bunny stream libraries player update`](/docs/cli/commands/stream#bunny-stream-libraries-player) changes colours, fonts, controls, playback speeds, and the heatmap: ```bash theme={null} bunny stream libraries player update my-library --color "#FF7755" --speeds 0.5,1.0,1.5,2.0 --heatmap ``` # Use Bunny Player with Astro Source: https://bunny.net/docs/stream/player/astro Embed the Bunny Stream player in an Astro site with a custom element, and control playback with player.js events and methods. Astro renders the Bunny Player iframe on the server and ships no JavaScript until you ask for some. This guide asks for very little: a `` custom element that attaches [player.js](https://github.com/embedly/player.js) and turns the player's events into DOM events. > **[Astro example on GitHub](https://github.com/BunnyWay/examples/tree/main/stream/player-astro)** > > An Astro site with custom controls and an event log. ## Quickstart **1. Install player.js** ```bash npm theme={null} npm install player.js ``` ```bash pnpm theme={null} pnpm add player.js ``` ```bash yarn theme={null} yarn add player.js ``` ```bash bun theme={null} bun add player.js ``` player.js ships without types. Add a declaration file anywhere your `tsconfig.json` includes, for example `player.js.d.ts`. It covers the methods and events the Bunny Player supports: ```ts player.js.d.ts theme={null} declare module "player.js" { export type PlayerEvent = | "ready" | "play" | "pause" | "ended" | "timeupdate" | "progress" | "seeked" | "error" | "playbackratechange"; export type TimeUpdate = { seconds: number; duration: number }; export type Progress = { percent: number; seconds: number; duration: number }; /** Present when a command fails. Empty when the media itself errors. */ export type PlayerError = { code: number; msg: string }; export class Player { constructor(iframe: HTMLIFrameElement | string); on(event: "ready", callback: () => void): void; on(event: "timeupdate", callback: (data: TimeUpdate) => void): void; on(event: "progress", callback: (data: Progress) => void): void; on(event: "playbackratechange", callback: (rate: number) => void): void; on(event: "error", callback: (error?: PlayerError) => void): void; on(event: PlayerEvent, callback: (data?: unknown) => void): void; off(event: PlayerEvent, callback?: (...args: never[]) => void): void; supports(kind: "method" | "event", name: string | string[]): boolean; /** Send a raw command, for methods player.js does not expose such as setPlaybackRate. Getters answer through the callback. */ send(message: { method: string; value?: unknown }, callback?: (value: unknown) => void): void; play(): void; pause(): void; mute(): void; unmute(): void; setVolume(percent: number): void; setCurrentTime(seconds: number): void; setLoop(loop: boolean): void; getPaused(callback: (paused: boolean) => void): void; getMuted(callback: (muted: boolean) => void): void; getVolume(callback: (percent: number) => void): void; getDuration(callback: (seconds: number) => void): void; getCurrentTime(callback: (seconds: number) => void): void; getLoop(callback: (loop: boolean) => void): void; } const playerjs: { Player: typeof Player; addEvent(elem: EventTarget, type: string, handler: EventListener): void; }; export default playerjs; } ``` **2. Create the component** The embed URL waits in `data-src` until the element has a `Player` listening. An iframe with `src` in the HTML can finish loading first, and then `ready` never arrives. ```astro src/components/BunnyPlayer.astro theme={null} --- type Props = { libraryId: string; videoId: string; params?: Record; title?: string; }; const { libraryId, videoId, params = {}, title = "Video player" } = Astro.props; const query = new URLSearchParams( Object.entries(params).map(([key, value]) => [key, String(value)]), ).toString(); const src = `https://player.mediadelivery.net/embed/${libraryId}/${videoId}${query ? `?${query}` : ""}`; --- ``` **3. Add your video IDs** Copy the library ID and video GUID from the video's page in the dashboard. Both are public, since they appear in every embed URL. ```bash .env theme={null} PUBLIC_BUNNY_LIBRARY_ID=12345 PUBLIC_BUNNY_VIDEO_ID=your-video-guid ``` Astro inlines `PUBLIC_` variables at build time. Without them, the embed URL reads `undefined/undefined`. **4. Add it to a page** ```astro src/pages/index.astro theme={null} --- import BunnyPlayer from "../components/BunnyPlayer.astro"; --- ``` `params` takes any [player parameter](/docs/stream/embedding#supported-parameters). ## Control playback `ready` hands over the `Player` as `event.detail`. ```astro theme={null} ``` Getters answer through a callback, since the value comes back from the iframe. Playback speed is missing from the npm build (0.1.0), and `send()` covers the gap: ```ts theme={null} player.send({ method: "setPlaybackRate", value: 1.5 }); ``` Browsers block unmuted `play()` before the viewer has clicked anything. Mute first if playback has to start on its own. The [Playback control API](/docs/stream/playback-api) lists every method and event. ## Track progress `timeupdate` fires several times a second. Throttle it before it reaches your backend. ```ts theme={null} import type { TimeUpdate } from "player.js"; let lastSaved = 0; document.querySelector("bunny-player")!.addEventListener("timeupdate", (event) => { const { seconds, duration } = (event as CustomEvent).detail; if (Math.abs(seconds - lastSaved) < 5) return; lastSaved = seconds; void saveProgress(seconds, duration); }); ``` Pass the saved position back as `params={{ t: savedSeconds }}` to resume. ## Signed embed URLs With [embed view token authentication](/docs/stream/token-authentication) on, sign the URL in the page frontmatter and pass `token` and `expires` through `params`. Render that page [on demand](https://docs.astro.build/en/guides/on-demand-rendering/). A prerendered page hands every visitor the same token, and it expires. The signing code is in [Sign embed URLs on the server](/docs/stream/player/signed-embeds). ## Load player.js from the CDN instead We host a build of player.js that adds `setPlaybackRate()` and the `playbackratechange` event ([Methods](/docs/stream/playback-api#methods)). Load it from the component, so only pages with a player fetch it. ```ts src/lib/load-playerjs.ts theme={null} type PlayerJs = (typeof import("player.js"))["default"]; declare global { interface Window { playerjs: PlayerJs; } } let loading: Promise | undefined; export function loadPlayerjs() { loading ??= new Promise((resolve, reject) => { const script = document.createElement("script"); script.src = "https://assets.mediadelivery.net/playerjs/playerjs-latest.min.js"; script.onload = () => resolve(window.playerjs); script.onerror = (error) => { loading = undefined; reject(error); }; document.head.append(script); }); return loading; } ``` Swap the `player.js` import in `BunnyPlayer.astro` for the loader, and wait for it before setting `src`. ```ts src/components/BunnyPlayer.astro theme={null} import { loadPlayerjs } from "../lib/load-playerjs"; async connectedCallback() { const iframe = this.querySelector("iframe"); const src = iframe?.dataset.src; if (!iframe || !src) return; delete iframe.dataset.src; const playerjs = await loadPlayerjs(); iframe.src = src; const player = new playerjs.Player(iframe); this.player = player; // ...register events as before } ``` ## Troubleshooting > **The ready event never fires** > > The iframe loaded before player.js was listening. Keep the URL in `data-src` and let the element set `src`. > > A hidden tab also holds `ready` back until the viewer switches to it. > **Controls stop working after a view transition** > > `` runs page scripts once, leaving your `ready` listener on the previous page's element. Register it inside an `astro:page-load` listener. > **The iframe shows a 403** > > The library's allowed domains, direct access block, or token authentication is rejecting the embed. See [Embedding restrictions](/docs/stream/embedding#embedding-restrictions). # Use Bunny Player with JavaScript Source: https://bunny.net/docs/stream/player/javascript Embed the Bunny Stream player with plain JavaScript or TypeScript, no framework, and control playback with player.js events and methods. With no framework, Bunny Player comes down to one function. It appends the iframe and returns a [player.js](https://github.com/embedly/player.js) `Player` for talking to it. > **[JavaScript example on GitHub](https://github.com/BunnyWay/examples/tree/main/stream/player-vanilla)** > > A framework-free Vite app with custom controls and an event log. ## Quickstart **1. Install player.js** ```bash npm theme={null} npm install player.js ``` ```bash pnpm theme={null} pnpm add player.js ``` ```bash yarn theme={null} yarn add player.js ``` ```bash bun theme={null} bun add player.js ``` player.js ships without types. Add a declaration file anywhere your `tsconfig.json` includes, for example `player.js.d.ts`. It covers the methods and events the Bunny Player supports: ```ts player.js.d.ts theme={null} declare module "player.js" { export type PlayerEvent = | "ready" | "play" | "pause" | "ended" | "timeupdate" | "progress" | "seeked" | "error" | "playbackratechange"; export type TimeUpdate = { seconds: number; duration: number }; export type Progress = { percent: number; seconds: number; duration: number }; /** Present when a command fails. Empty when the media itself errors. */ export type PlayerError = { code: number; msg: string }; export class Player { constructor(iframe: HTMLIFrameElement | string); on(event: "ready", callback: () => void): void; on(event: "timeupdate", callback: (data: TimeUpdate) => void): void; on(event: "progress", callback: (data: Progress) => void): void; on(event: "playbackratechange", callback: (rate: number) => void): void; on(event: "error", callback: (error?: PlayerError) => void): void; on(event: PlayerEvent, callback: (data?: unknown) => void): void; off(event: PlayerEvent, callback?: (...args: never[]) => void): void; supports(kind: "method" | "event", name: string | string[]): boolean; /** Send a raw command, for methods player.js does not expose such as setPlaybackRate. Getters answer through the callback. */ send(message: { method: string; value?: unknown }, callback?: (value: unknown) => void): void; play(): void; pause(): void; mute(): void; unmute(): void; setVolume(percent: number): void; setCurrentTime(seconds: number): void; setLoop(loop: boolean): void; getPaused(callback: (paused: boolean) => void): void; getMuted(callback: (muted: boolean) => void): void; getVolume(callback: (percent: number) => void): void; getDuration(callback: (seconds: number) => void): void; getCurrentTime(callback: (seconds: number) => void): void; getLoop(callback: (loop: boolean) => void): void; } const playerjs: { Player: typeof Player; addEvent(elem: EventTarget, type: string, handler: EventListener): void; }; export default playerjs; } ``` **2. Create the player function** The `Player` has to exist before the iframe finishes loading. Creating both in one function takes care of the order. ```ts src/bunny-player.ts theme={null} import playerjs, { type Player } from "player.js"; export type BunnyPlayerOptions = { libraryId: string; videoId: string; params?: Record; title?: string; }; export function createBunnyPlayer( container: HTMLElement, { libraryId, videoId, params, title = "Video player" }: BunnyPlayerOptions, ): Player { const query = new URLSearchParams( Object.entries(params ?? {}).map(([key, value]) => [key, String(value)]), ).toString(); const iframe = document.createElement("iframe"); iframe.src = `https://player.mediadelivery.net/embed/${libraryId}/${videoId}${query ? `?${query}` : ""}`; iframe.title = title; iframe.loading = "lazy"; iframe.allow = "autoplay; encrypted-media; picture-in-picture; fullscreen"; iframe.allowFullscreen = true; iframe.style.cssText = "display: block; width: 100%; aspect-ratio: 16 / 9; border: 0; background: #000;"; container.append(iframe); return new playerjs.Player(iframe); } ``` **3. Mount it** ```ts src/main.ts theme={null} import { createBunnyPlayer } from "./bunny-player"; const player = createBunnyPlayer(document.querySelector("#player")!, { libraryId: "12345", videoId: "your-video-guid", params: { preload: true }, }); player.on("ready", () => console.log("ready")); ``` `params` takes any [player parameter](/docs/stream/embedding#supported-parameters). ## Control playback ```ts theme={null} player.on("ready", () => { document.querySelector("#toggle")!.addEventListener("click", () => { // Ask the player, because viewers can also use its own controls. player.getPaused((paused) => (paused ? player.play() : player.pause())); }); document.querySelector("#restart")!.addEventListener("click", () => player.setCurrentTime(0)); }); ``` Getters answer through a callback, since the value comes back from the iframe. Playback speed is missing from the npm build (0.1.0), and `send()` covers the gap: ```ts theme={null} player.send({ method: "setPlaybackRate", value: 1.5 }); ``` Browsers block unmuted `play()` before the viewer has clicked anything. Mute first if playback has to start on its own. The [Playback control API](/docs/stream/playback-api) lists every method and event. ## Track progress `timeupdate` fires several times a second. Throttle it before it reaches your backend. ```ts theme={null} let lastSaved = 0; player.on("timeupdate", ({ seconds, duration }) => { if (Math.abs(seconds - lastSaved) < 5) return; lastSaved = seconds; void saveProgress(seconds, duration); }); ``` Pass the saved position back as `params: { t: savedSeconds }` to resume. ## Use it without a bundler We host a build of player.js that adds `setPlaybackRate()` and the `playbackratechange` event ([Methods](/docs/stream/playback-api#methods)). Classic scripts run as the browser parses them, which puts both scripts ahead of the iframe's load. ```html theme={null} ``` ## Troubleshooting > **The ready event never fires** > > The iframe loaded before player.js was listening. Module and `defer` scripts run after parsing, by which time an iframe in the HTML may be done. Create the iframe from script, as `createBunnyPlayer` does. > > A hidden tab also holds `ready` back until the viewer switches to it. > **Events fire twice after replacing the video** > > Each `Player` adds a `message` listener to `window` that player.js never removes. Capture it by wrapping `playerjs.addEvent` while you construct the `Player`, then remove it with the iframe, as the [React guide](/docs/stream/player/react) does. > **The iframe shows a 403** > > The library's allowed domains, direct access block, or token authentication is rejecting the embed. See [Embedding restrictions](/docs/stream/embedding#embedding-restrictions). # Use Bunny Player with Next.js Source: https://bunny.net/docs/stream/player/nextjs Embed the Bunny Stream player in a Next.js app with a client component that survives server rendering, and control playback with player.js. [player.js](https://github.com/embedly/player.js) controls the Bunny Player iframe, and server rendering gives it two rules. It reads `window` on import, which means importing it in the browser. It also has to load before the iframe does. player.js caches the iframe's `ready` message on import, and a `Player` created any time after that still connects. An iframe that finishes loading first, straight from the server HTML, never becomes ready. The component below renders a placeholder on the server, loads player.js in an effect, then mounts the iframe and creates the `Player` in the same commit. It works with the App Router and the Pages Router. For a client-only app, see the [React guide](/docs/stream/player/react). > **[Next.js example on GitHub](https://github.com/BunnyWay/examples/tree/main/stream/player-nextjs)** > > An App Router app with custom controls and an event log. ## Quickstart **1. Install player.js** ```bash npm theme={null} npm install player.js ``` ```bash pnpm theme={null} pnpm add player.js ``` ```bash yarn theme={null} yarn add player.js ``` ```bash bun theme={null} bun add player.js ``` player.js ships without types. Add a declaration file anywhere your `tsconfig.json` includes, for example `player.js.d.ts`. It covers the methods and events the Bunny Player supports: ```ts player.js.d.ts theme={null} declare module "player.js" { export type PlayerEvent = | "ready" | "play" | "pause" | "ended" | "timeupdate" | "progress" | "seeked" | "error" | "playbackratechange"; export type TimeUpdate = { seconds: number; duration: number }; export type Progress = { percent: number; seconds: number; duration: number }; /** Present when a command fails. Empty when the media itself errors. */ export type PlayerError = { code: number; msg: string }; export class Player { constructor(iframe: HTMLIFrameElement | string); on(event: "ready", callback: () => void): void; on(event: "timeupdate", callback: (data: TimeUpdate) => void): void; on(event: "progress", callback: (data: Progress) => void): void; on(event: "playbackratechange", callback: (rate: number) => void): void; on(event: "error", callback: (error?: PlayerError) => void): void; on(event: PlayerEvent, callback: (data?: unknown) => void): void; off(event: PlayerEvent, callback?: (...args: never[]) => void): void; supports(kind: "method" | "event", name: string | string[]): boolean; /** Send a raw command, for methods player.js does not expose such as setPlaybackRate. Getters answer through the callback. */ send(message: { method: string; value?: unknown }, callback?: (value: unknown) => void): void; play(): void; pause(): void; mute(): void; unmute(): void; setVolume(percent: number): void; setCurrentTime(seconds: number): void; setLoop(loop: boolean): void; getPaused(callback: (paused: boolean) => void): void; getMuted(callback: (muted: boolean) => void): void; getVolume(callback: (percent: number) => void): void; getDuration(callback: (seconds: number) => void): void; getCurrentTime(callback: (seconds: number) => void): void; getLoop(callback: (loop: boolean) => void): void; } const playerjs: { Player: typeof Player; addEvent(elem: EventTarget, type: string, handler: EventListener): void; }; export default playerjs; } ``` **2. Create the client component** ```tsx components/bunny-player.tsx theme={null} "use client"; import { useEffect, useRef, useState, type CSSProperties } from "react"; import type { Player, TimeUpdate } from "player.js"; type PlayerJs = (typeof import("player.js"))["default"]; export type BunnyPlayerProps = { libraryId: string; videoId: string; params?: Record; title?: string; onReady?: (player: Player) => void; onPlay?: () => void; onPause?: () => void; onEnded?: () => void; onTimeUpdate?: (time: TimeUpdate) => void; }; const frameStyle: CSSProperties = { display: "block", width: "100%", height: "auto", aspectRatio: "16 / 9", border: 0, background: "#000", }; export function BunnyPlayer({ libraryId, videoId, params, title = "Video player", onReady, onPlay, onPause, onEnded, onTimeUpdate, }: BunnyPlayerProps) { const iframeRef = useRef(null); const [playerjs, setPlayerjs] = useState(null); // Keep the latest callbacks without re-creating the player. const handlers = useRef({ onReady, onPlay, onPause, onEnded, onTimeUpdate }); useEffect(() => { handlers.current = { onReady, onPlay, onPause, onEnded, onTimeUpdate }; }); // player.js reads window when imported, so load it in the browser only. useEffect(() => { let cancelled = false; import("player.js").then((mod) => { if (!cancelled) setPlayerjs(mod.default); }); return () => { cancelled = true; }; }, []); const query = new URLSearchParams( Object.entries(params ?? {}).map(([key, value]) => [key, String(value)]), ).toString(); const src = `https://player.mediadelivery.net/embed/${libraryId}/${videoId}${query ? `?${query}` : ""}`; useEffect(() => { const iframe = iframeRef.current; if (!playerjs || !iframe) return; // player.js never removes the window listener each Player adds, // so grab it while constructing and remove it on cleanup. let onMessage: EventListener = () => {}; const addEvent = playerjs.addEvent; playerjs.addEvent = (elem, type, handler) => addEvent(elem, type, (onMessage = handler)); const player = new playerjs.Player(iframe); playerjs.addEvent = addEvent; player.on("ready", () => handlers.current.onReady?.(player)); player.on("play", () => handlers.current.onPlay?.()); player.on("pause", () => handlers.current.onPause?.()); player.on("ended", () => handlers.current.onEnded?.()); player.on("timeupdate", (time) => handlers.current.onTimeUpdate?.(time)); return () => window.removeEventListener("message", onMessage); }, [playerjs, src]); // Hold the space until player.js is loaded so the iframe cannot // finish loading before the Player exists. if (!playerjs) { return