DEV Community

Cover image for allow_sales: false
alfchee
alfchee

Posted on

allow_sales: false

allow_sales: false

That's it. That's the reason I can't charge money for my software.

Not a rejected application. Not a compliance review that went badly. A boolean field, for country code NI, in a payment provider's configuration. Signups blocked, transactions blocked, at the API layer. Nothing to appeal, because nothing was ever considered.

I want to write down what I found, because I went looking for this article before I wrote it and it doesn't exist.


The app, briefly, and the elephant

I built a shared personal-finance tracker for couples. My wife and I needed one, Honeydue isn't available in Nicaragua, and every alternative assumes you can connect your bank through Plaid — which covers the US and parts of Europe and nothing where I live.

Yes: another personal finance app. I know. It's the modern to-do list, the thing every developer builds now that an agent can scaffold a CRUD with charts in an afternoon. If you came here for that, there's nothing for you.

The app isn't the story. The story starts when I tried to charge for it.


The sentence every tutorial ends with

"Now just add Stripe and you're done."

I've read that sentence in maybe two hundred tutorials, and I never once noticed that it's a geographic claim disguised as a technical step.

Stripe isn't available to sellers in Nicaragua. Fine — that's what merchant-of-record platforms are for. An MoR sells to your customer on your behalf, absorbs the cross-border sales tax problem, and pays you out. That's the standard answer for developers outside the US and EU, and there's a healthy market of them now.

So I worked through the market. Here's what I found.


Five providers

Dodo Payments

I created the account first and read the docs second, which was my mistake.

Nicaragua isn't on their accepted-countries list. What made this land was the shape of the omission: Belize, Costa Rica, El Salvador, Guatemala, Honduras and Panama are all there. Every country I share a border or a region with. Not mine.

Their eligibility rule is the important detail, and it's the one that kills the obvious workaround:

Eligibility is based on the country that issued the government-issued identity document you verify with, not on where your company is registered or where you pay tax.

So "just incorporate in Panama" doesn't work. For a registered entity, every director and beneficial owner has to verify with an ID from a listed country. A Panamanian company with me as its sole owner fails the same check.

Their failure paths are also one-shot, which matters more than it sounds:

  • Identity verification does not reopen automatically after a failure — you have to email support and ask
  • A form marked Failed gets you one appeal, and the appeal decision is final
  • And: "If your business could not be supported for reasons of location or product category, an appeal cannot change that outcome" Read those together. If I had submitted the KYC form and let it fail on country, I'd have burned my single appeal on a question the appeal process explicitly cannot answer. Their docs actually tell you to ask first — buried in an info box that I'm glad I read:

Not sure how this applies to you? Contact support before you begin your integration, so you don't build against an outcome that cannot be approved.

Creem

Creem publishes a list of 32 countries it can't accept payments from — merchants or customers. Nicaragua is on it.

Here's the company it keeps: Afghanistan, Belarus, Cuba, Iran, Iraq, Libya, North Korea, Russia, Somalia, Sudan, Syria, Venezuela, Yemen.

That list told me more than the other four providers combined. This isn't a coverage gap that someone will get around to filling. It's a risk classification, and my country is in a bucket with active war zones and comprehensively sanctioned states.

Polar

Polar's payments are global — "except from countries with US sanctions (Cuba, Russia, Iran, North Korea, and Syria)." Nicaragua isn't in that list, which was briefly encouraging.

Then I read the payouts section. Polar pays out through Stripe Connect Express to a specific list of countries. Costa Rica, El Salvador, Guatemala, Panama. No Nicaragua, no Honduras.

So customers in Nicaragua can pay Polar. Polar cannot pay me. Being able to take money you can never collect is a strange kind of "supported".

Lemon Squeezy

Two payout methods: bank transfer and PayPal.

The bank-payout list excludes Nicaragua. The PayPal branch stays theoretically open, since they pay out to PayPal in 200+ countries — but PayPal in Nicaragua has historically been send-only, and "you can pay with it but not get paid into it" is the local default for a lot of financial products.

Their eligibility rule is refreshingly blunt:

If you don't see your country listed by PayPal or in the list below, sorry, you won't be able to use Lemon Squeezy.

Moot anyway: since the Stripe acquisition, new merchant access is waitlist-only.

Paddle

Paddle was the last real candidate, and the most promising one — it's the only MoR I found that supports Payoneer as a payout method natively, which mattered because Payoneer is one of the few financial rails that reaches me.

I asked their assistant directly. The answer:

The knowledge sources explicitly list Nicaragua (NI) as a country where allow_sales is false, meaning Paddle blocks both signups and transactions from Nicaragua. [...] Paddle's API actively prevents signups from unsupported/sanctioned countries.

That's the field name. That's the whole conversation.


Five for five

Provider Result
Dodo Payments Nicaragua absent from accepted countries
Creem Explicitly unsupported — listed with Cuba, Iran, Syria
Polar Payments yes, payouts no
Lemon Squeezy Bank list excludes it; invite-only anyway
Paddle allow_sales = false at the API

The part I want other developers to understand

This is de-risking, not sanctions.

Nicaragua is not comprehensively sanctioned the way Cuba, Iran, North Korea and Syria are. US measures target named individuals and entities, not a blanket prohibition on Nicaraguan people receiving payment for software. There is no law that says a payment company can't onboard me.

What's happening is that every one of these providers sits on top of the same handful of acquirers, banks and card networks, and those upstream partners apply risk scoring built from FATF, OFAC and EU AML frameworks. When a country scores badly, the cheapest decision for everyone downstream is a boolean. Nobody in the chain has to decide I'm a risk. The chain just never gets to me.

Two consequences worth internalising:

It's a commercial decision, which means it's a conversation. A legal prohibition is a wall. A risk policy is a threshold, and thresholds have exceptions and appeals and humans behind them. That's why the country lists differ between providers who are otherwise near-identical, and why asking directly is worth doing even when the list says no.

It's invisible from the other side. Every payments tutorial, every "ship your SaaS in a weekend" course, every Stripe integration guide, is written by someone for whom this problem doesn't exist. Not out of malice — they genuinely don't know the layer is there. Which is exactly why nobody writes this down.


The workaround I didn't take

Every forum thread on this converges on the same two suggestions.

Use a US receiving account. Payoneer and Wise give you real US/EU account details in your own name. Enter those as your bank account and the country check appears to vanish.

Have someone abroad be the merchant. A friend or relative in a supported country signs up, receives the money, forwards it on.

Both are misrepresentation to the provider, and I want to be specific about why I said no, because "it's against the rules" isn't a real argument to someone who's been told no five times.

The failure mode is the worst shape available. It doesn't fail at signup, when failing costs you nothing. It fails at the first payout, or the first compliance review, or the first chargeback — with customer money already collected, an account you can no longer withdraw from, and subscribers whose payments you now have to unwind. Not being able to get paid is a problem. Being paid and then frozen while you owe refunds to real people is a worse one.

And for the second option, you've added someone else's identity, tax position and banking relationship to your risk surface, permanently.

The distinction I settled on: giving a provider a Payoneer account when they ask for one and you've told them where you live is completely fine — that's exactly why Paddle was my last hope. Using one to appear domiciled somewhere I'm not is a different thing entirely.


What I built instead

Here's the part that's actually useful if you're anywhere in this situation: you can build your entire billing domain against zero payment providers, and you should, because the answer to "which provider" may take months and shouldn't dictate your architecture.

One port

export interface PaymentProvider {
  readonly id: string;

  createCheckout(input: {
    householdId: string;
    planCode: string;
    idempotencyKey: string;
  }): Promise<{ redirectUrl?: string; clientToken?: string; reference: string }>;

  cancelSubscription(input: { subscriptionRef: string }): Promise<void>;

  // Authoritative state, for reconciliation against local records.
  getSubscription(input: { subscriptionRef: string }): Promise<ProviderSubscription>;

  // Verify and normalise an inbound webhook into internal domain events.
  parseWebhook(req: Request): Promise<BillingEvent[]>;
}
Enter fullscreen mode Exit fullscreen mode

The critical design choice is BillingEvent. It's my vocabulary, not a vendor's:

type BillingEvent =
  | { type: "subscription.activated"; ref: string; planCode: string; periodEnd: Date }
  | { type: "payment.succeeded";      ref: string; amount: Money }
  | { type: "payment.failed";         ref: string; attempt: number }
  | { type: "subscription.cancelled"; ref: string; effectiveAt: Date }
  | { type: "subscription.expired";   ref: string };
Enter fullscreen mode Exit fullscreen mode

Every adapter translates into that set. Entitlements, grace periods and dunning only ever see these five events. No stripe. or paddle. prefix reaches the domain layer, which means switching providers is writing one file.

One stub

export class StubPaymentProvider implements PaymentProvider {
  readonly id = "stub";
  constructor(private clock: Clock, private store: StubStore) {}

  // Test-only: drive the lifecycle deterministically.
  async advance(ref: string, to: SubscriptionStatus): Promise<BillingEvent[]> { /* ... */ }
  async simulateFailedRenewal(ref: string): Promise<BillingEvent[]> { /* ... */ }
  async redeliver(eventId: string): Promise<BillingEvent[]> { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

With an injectable clock, a 30-day trial and a 7-day grace period run in milliseconds. I can test the full path — trialing → active → past_due → grace → cancelled → expired, plus reactivation, duplicate webhooks and out-of-order delivery — with no provider, no sandbox, and no account.

Those are the parts of billing that are hardest to get right and most expensive to get wrong, and they're now covered by tests that ran before I knew who would process a payment.

The bug this design already caught

While building the entitlement helpers I wrote a function returning a feature limit from a config table. If the row was missing it returned NULL. In the trigger that enforced the limit, the comparison count >= limit against NULL evaluates to NULL — not true — so the insert proceeded.

A missing configuration row meant unlimited, silently, on the paid feature. Fail-closed isn't a slogan; coalesce it to zero.

I found that with the stub, in a test, months before any money existed. That's the argument for this approach better than anything I can say about hexagonal architecture.


Where I ended up

Local acquiring. My wife runs a business with a year of clean payment-processing history at a regional bank, so the route is a proper merchant account with the correct business classification — asked for openly, not borrowed.

It's a genuinely different deal, and worth knowing the shape of before you go looking for it. A local acquirer is not a merchant of record. I become the seller: sales tax is mine, invoicing is mine, chargebacks are mine. Which means cross-border selling stops being free — without an MoR absorbing it, selling a subscription into six countries makes me a tax filer in six countries.

So the paid tier launches domestic only. Not as an apology. It's the correct scope, and it happens to match who actually uses the app.


If you're reading this from a country like mine

  1. Find the accepted-countries page before you build the pricing page. It's usually three clicks deep and it's the single highest-information document about your project's viability.
  2. Ask compliance directly rather than inferring from a list. These are risk decisions, not laws. The answers differ between providers and sometimes between applicants.
  3. Don't submit a KYC form you expect to fail. Appeals are often one-shot, and a location rejection can't be appealed anyway.
  4. Design the provider boundary first. Then the answer, whenever it arrives, is one adapter instead of a rewrite.

5. Check whether your users can pay before checking whether you can be paid. Card penetration and recurring-charge support in your market may constrain the model more than any API does.

The code was never the hard part. An agent could have built the app.

The hard part is that "just add Stripe and you're done" is a sentence written by people for whom it's true, and if you're not one of them, nobody writes down what happens next.

So — here it is.


I'm a solution architect and DevOps engineer in Nicaragua. The app is a shared finance tracker for couples that my wife and I actually use; the repo is public because the interesting part is the decisions, not the CRUD. If you've hit this same wall from somewhere else, I'd like to hear how it went — the more of these that get written down, the less time the next person loses.

Top comments (0)