WooCommerce and card payments in Serbia: what actually needs preparing
The company paperwork, the store pages and the checkout details a Serbian WooCommerce shop needs in place before card payments are approved and go live.
Marko Stančić8 minutes
Field note / Payment readiness
Card payments in a Serbian shop don't get switched on with a plugin. Before a bank or a processor approves the store you need a registered company, a business account and a signed card acceptance contract, and by then the site must already carry terms of sale, a privacy policy, a complaint and refund procedure, delivery terms and deadlines, prices in dinars with the VAT status stated, and full company identification.
Two jobs run in parallel here, and they don't finish together. Integrating a WooCommerce store with a payment gateway is a measurable task with a clear end. Merchant approval is not: it depends on what somebody sees when they open your site with no explanation from you. Below is what has to exist before the application goes in, and what the integration most often gets wrong.
What must exist before anyone looks at the site
Accepting cards is a financial service, contracted with an institution rather than with software. The base requirements are the same whoever you approach:
A registered company or sole trader whose registered activity covers what you actually sell.
A business account where the money lands after settlement.
A card acceptance contract with an acquiring bank, or with a payment processor that handles that side for you.
Know your customer paperwork: an extract from the business register (APR), details of the authorised person and the beneficial owners, and the tax number (PIB).
A description of the catalogue, a realistic estimate of average order value and monthly turnover, and the address of the site that goes live.
The first decision with technical consequences is made here. Contracting directly with a bank, with Banca Intesa's web shop service for example, means one relationship, one integration and one counterparty to negotiate fees with. A processor such as CorvusPay sits between the store and the acquirer and gives you one technical connection for several card brands, a maintained WooCommerce module and its own support when a transaction goes sideways.
Neither is cheaper by default. Compare the fee per transaction, the fixed monthly cost, how long it takes for the money to reach the account, and how much work stays with you when something goes wrong. Ask what is covered: DinaCard (the domestic card scheme), Visa and Mastercard, payment in instalments, the IPS QR code for instant bank transfers, and whether partial refunds and refunds made after settlement run through the same channel or by hand. That last question looks minor until the day you need it.
One planning note: don't tie a launch date to an approval date you don't control. The store should be able to trade with cash on delivery and bank transfer from day one, and cards can go on when approval arrives. That way the launch doesn't sit in somebody else's queue.
What the reviewer opens on the site
When the application arrives, somebody opens the site as an ordinary visitor and looks for specific things. They don't read the marketing copy and they won't call to ask. If they can't find the following in a few minutes, the application comes back:
Terms of sale written for your store, with your own company name in the text.
A privacy policy saying what you collect, why, how long you keep it and who you pass it to, the processor included.
A complaint and refund procedure: how a complaint is filed, within what deadline you answer, how the money gets back to the customer.
Delivery terms and deadlines, with the delivery cost and the territory you cover.
Prices in dinars with the VAT status marked. If you're not in the VAT system, that has to be stated too.
Full company identification: business name, registered address, company registration number (matični broj) and tax number, reachable from every page, usually in the footer.
Contact details that work: a phone number and an email address, not only a form.
The card logos you accept and the transaction security notice in the wording the bank requires, plus a statement of the currency you actually charge in, if you also show prices in another currency.
Those identification details are not a bank preference but a legal obligation for a company, so there's nothing to negotiate there. Two deadlines are most often copied wrong: the consumer's right to withdraw from a distance contract within fourteen days, and the deadline for answering a complaint once it's filed. Open the current Consumer Protection Act and write the real deadlines in, rather than lifting the wording from another site. Copied terms of sale with someone else's company name still in them are the fastest way to have an application returned.
Where those pages sit matters too. They belong in the footer of every page, not only in the last step of checkout, and the terms checkbox has to link to them with a link that works.
Working principle
A plugin doesn't get the shop approved. What the site says when somebody opens it cold does.
Why applications get sent back
The reasons repeat, and almost none of them are technically hard:
The site is still behind a password or in maintenance mode, or a staging address was submitted instead of the production one.
Prices are shown only in euros, or VAT appears for the first time in the final checkout step.
The registered address and the registration numbers appear nowhere on the site.
The delivery page and the terms of sale quote different deadlines.
The catalogue doesn't match the registered activity and the description in the application.
The SSL certificate has expired, or part of the site still loads over an unsecured connection.
Nobody answers the phone number on the site, and mail to the address it gives bounces back.
All of it is cheap while the store is being built and expensive once the application comes back, because that means waiting out another round of review. When we take on WooCommerce development, these items go into the plan alongside the catalogue and the checkout, not after launch.
HTTPS, 3-D Secure and the payment form
HTTPS covers the whole site, not just the cart. Mixed content is enough to fail the check: one old image, or a script loaded over an unsecured connection. The certificate needs automatic renewal and monitoring, because an expired one takes down the processor's callback as well as the site, and you notice only when orders stop moving.
3-D Secure 2 is the default now. The customer is authenticated by their issuing bank, not by you: sometimes with no extra step, sometimes by confirming the purchase in a banking app or with a code sent by SMS. The store doesn't choose which, and it must not assume the customer stays in the same tab and the same session.
That leads to the second decision: a redirect, or a form on your own domain. A redirect sends the customer to the processor's page, keeps your PCI DSS scope at the lightest questionnaire and keeps card data out of your hands, but the design of that page is not yours. A hosted form, or card fields embedded in a frame on your domain, keeps the customer with you and gives a smoother flow, while your compliance scope grows, because the page that collects the data is now put together on your site. Off the table either way: a card number passing through your server or your database.
One detail breaks integrations specifically in production. The return from the bank often arrives as a POST request from another domain, and under the default SameSite rule the session cookie is not sent with it, so the cart and the session look empty even though the money has been taken. The order has to be found by the identifier and key carried in that request, never by the session.
Order statuses: a paid order must never stay pending
This is where integrations fail quietly. WooCommerce has its own statuses (pending, processing, completed, cancelled, failed, refunded), the processor has its own response codes, and somebody has to write the translation between the two vocabularies. The rule is simple: the source of truth is the processor's server-side notification, not the customer's return in the browser. Customers close the tab, lose the network, or come back five minutes later. If the status changes only on the return URL, some paid orders will sit in a status that looks unpaid until somebody hunts for them in the processor's report.
// The server notification is the source of truth, the customer return is not.
$order = wc_get_order( $orderId );
if ( ! $order || ! $order->needs_payment() ) {
return; // already handled, cancelled or duplicated
}
if ( 'approved' === $responseStatus && $order->get_total() == $amount ) {
$order->payment_complete( $transactionId );
} else {
$order->update_status( 'failed', 'Processor: ' . $responseCode );
}
Three things are easy to miss. Handling the notification has to be idempotent, because processors do send it twice. The amount and the currency in the notification have to be checked against the order before the payment is confirmed. And every processor response belongs in the order notes, so that a complaint a month later has a trail nobody has to dig out of the server logs.
Check too that the callback address can receive the request at all. A firewall, a rule blocking POST from unknown addresses, or a maintenance mode plugin can reject the processor's call without a single error message, and the store looks fine until someone compares the reports.
The test environment gives you test cards, responses you can force to approved or declined, and the exact shape of the notification. That's enough to write the logic, not enough to trust it. Production is where you first meet the real 3-D Secure step on the customer's phone, an issuing bank that answers slowly, a double click on the pay button, a partial refund, and a daily report that has to reconcile with the bank statement. Once the production keys are in place, run one real transaction with your own card and refund it through the same channel, before telling anyone that payments are live.
A payment integration is finished when refunds work, not when the first charge goes through.
Cash on delivery doesn't disappear, it runs alongside
Cards don't replace cash on delivery in this market. Plenty of customers still pay the courier, and that is habit and trust rather than a flaw in the checkout. Adding cards shifts the balance between the two rather than removing one of them.
The consequence is two order flows in one system. With a card the money is taken before dispatch, the order moves into processing automatically, and a refund runs through the processor and leaves a trail. With cash on delivery the money is collected at the door, so until then the order is an obligation rather than revenue, a parcel refused on the doorstep is a real cost, and a refund is a separate procedure. Statuses, reports and the instructions your team works from have to reflect that, or the warehouse and the books drift apart inside the first week.
In the Cvetam flower delivery store, card payments through Banca Intesa run alongside cash on delivery, in production since 2022. Checkout treats neither as the exception: both sit in the same place, described in the same language, with the same number of steps. Someone ordering a bouquet at four in the afternoon shouldn't have to choose between speed and habit.
A decision framework for shop owners weighing a plugin, more configuration or a few lines of custom checkout code, and the honest default for most shops.
Field note / Payment readiness
Card payments in a Serbian shop don't get switched on with a plugin. Before a bank or a processor approves the store you need a registered company, a business account and a signed card acceptance contract, and by then the site must already carry terms of sale, a privacy policy, a complaint and refund procedure, delivery terms and deadlines, prices in dinars with the VAT status stated, and full company identification.
Two jobs run in parallel here, and they don't finish together. Integrating a WooCommerce store with a payment gateway is a measurable task with a clear end. Merchant approval is not: it depends on what somebody sees when they open your site with no explanation from you. Below is what has to exist before the application goes in, and what the integration most often gets wrong.
What must exist before anyone looks at the site
Accepting cards is a financial service, contracted with an institution rather than with software. The base requirements are the same whoever you approach:
The first decision with technical consequences is made here. Contracting directly with a bank, with Banca Intesa's web shop service for example, means one relationship, one integration and one counterparty to negotiate fees with. A processor such as CorvusPay sits between the store and the acquirer and gives you one technical connection for several card brands, a maintained WooCommerce module and its own support when a transaction goes sideways.
Neither is cheaper by default. Compare the fee per transaction, the fixed monthly cost, how long it takes for the money to reach the account, and how much work stays with you when something goes wrong. Ask what is covered: DinaCard (the domestic card scheme), Visa and Mastercard, payment in instalments, the IPS QR code for instant bank transfers, and whether partial refunds and refunds made after settlement run through the same channel or by hand. That last question looks minor until the day you need it.
One planning note: don't tie a launch date to an approval date you don't control. The store should be able to trade with cash on delivery and bank transfer from day one, and cards can go on when approval arrives. That way the launch doesn't sit in somebody else's queue.
What the reviewer opens on the site
When the application arrives, somebody opens the site as an ordinary visitor and looks for specific things. They don't read the marketing copy and they won't call to ask. If they can't find the following in a few minutes, the application comes back:
Those identification details are not a bank preference but a legal obligation for a company, so there's nothing to negotiate there. Two deadlines are most often copied wrong: the consumer's right to withdraw from a distance contract within fourteen days, and the deadline for answering a complaint once it's filed. Open the current Consumer Protection Act and write the real deadlines in, rather than lifting the wording from another site. Copied terms of sale with someone else's company name still in them are the fastest way to have an application returned.
Where those pages sit matters too. They belong in the footer of every page, not only in the last step of checkout, and the terms checkbox has to link to them with a link that works.
A plugin doesn't get the shop approved. What the site says when somebody opens it cold does.
Why applications get sent back
The reasons repeat, and almost none of them are technically hard:
All of it is cheap while the store is being built and expensive once the application comes back, because that means waiting out another round of review. When we take on WooCommerce development, these items go into the plan alongside the catalogue and the checkout, not after launch.
HTTPS, 3-D Secure and the payment form
HTTPS covers the whole site, not just the cart. Mixed content is enough to fail the check: one old image, or a script loaded over an unsecured connection. The certificate needs automatic renewal and monitoring, because an expired one takes down the processor's callback as well as the site, and you notice only when orders stop moving.
3-D Secure 2 is the default now. The customer is authenticated by their issuing bank, not by you: sometimes with no extra step, sometimes by confirming the purchase in a banking app or with a code sent by SMS. The store doesn't choose which, and it must not assume the customer stays in the same tab and the same session.
That leads to the second decision: a redirect, or a form on your own domain. A redirect sends the customer to the processor's page, keeps your PCI DSS scope at the lightest questionnaire and keeps card data out of your hands, but the design of that page is not yours. A hosted form, or card fields embedded in a frame on your domain, keeps the customer with you and gives a smoother flow, while your compliance scope grows, because the page that collects the data is now put together on your site. Off the table either way: a card number passing through your server or your database.
One detail breaks integrations specifically in production. The return from the bank often arrives as a POST request from another domain, and under the default SameSite rule the session cookie is not sent with it, so the cart and the session look empty even though the money has been taken. The order has to be found by the identifier and key carried in that request, never by the session.
Order statuses: a paid order must never stay pending
This is where integrations fail quietly. WooCommerce has its own statuses (pending, processing, completed, cancelled, failed, refunded), the processor has its own response codes, and somebody has to write the translation between the two vocabularies. The rule is simple: the source of truth is the processor's server-side notification, not the customer's return in the browser. Customers close the tab, lose the network, or come back five minutes later. If the status changes only on the return URL, some paid orders will sit in a status that looks unpaid until somebody hunts for them in the processor's report.
Three things are easy to miss. Handling the notification has to be idempotent, because processors do send it twice. The amount and the currency in the notification have to be checked against the order before the payment is confirmed. And every processor response belongs in the order notes, so that a complaint a month later has a trail nobody has to dig out of the server logs.
Check too that the callback address can receive the request at all. A firewall, a rule blocking POST from unknown addresses, or a maintenance mode plugin can reject the processor's call without a single error message, and the store looks fine until someone compares the reports.
The test environment gives you test cards, responses you can force to approved or declined, and the exact shape of the notification. That's enough to write the logic, not enough to trust it. Production is where you first meet the real 3-D Secure step on the customer's phone, an issuing bank that answers slowly, a double click on the pay button, a partial refund, and a daily report that has to reconcile with the bank statement. Once the production keys are in place, run one real transaction with your own card and refund it through the same channel, before telling anyone that payments are live.
Cash on delivery doesn't disappear, it runs alongside
Cards don't replace cash on delivery in this market. Plenty of customers still pay the courier, and that is habit and trust rather than a flaw in the checkout. Adding cards shifts the balance between the two rather than removing one of them.
The consequence is two order flows in one system. With a card the money is taken before dispatch, the order moves into processing automatically, and a refund runs through the processor and leaves a trail. With cash on delivery the money is collected at the door, so until then the order is an obligation rather than revenue, a parcel refused on the doorstep is a real cost, and a refund is a separate procedure. Statuses, reports and the instructions your team works from have to reflect that, or the warehouse and the books drift apart inside the first week.
In the Cvetam flower delivery store, card payments through Banca Intesa run alongside cash on delivery, in production since 2022. Checkout treats neither as the exception: both sit in the same place, described in the same language, with the same number of steps. Someone ordering a bouquet at four in the afternoon shouldn't have to choose between speed and habit.