TL;DR
A payment plugin should arrive with no API keys in it, and the Paymos package does: the archive is identical for every merchant and carries no API key, API secret, project ID, webhook secret, OAuth token, or device code. Setup does not add them by hand either — connecting a store is one authorization, and the keys, the webhook, and the project binding are all provisioned in that single step. A package assembled per merchant buys a shorter setup by putting a live secret into a file that then travels into backups, support tickets, and version control. Two checks — a search and a checksum — tell you which kind you were handed.
A Paymos plugin package carries no credentials. The archive a merchant downloads for WooCommerce, Magento 2, or any of the other six official plugins is the same archive every other merchant downloads, and no API key, API secret, project ID, webhook secret, OAuth token, or device code sits inside it. Downloading it connects nothing.
No later step puts them there either. Connecting a store is a single authorization, and the keys, the webhook, and the project binding are all provisioned in that one step, then held encrypted on the store's side.
Keeping the archive empty is the point rather than a detail. A package assembled per merchant, with the keys already written in, is a live credential in a file — and a file travels: into a Downloads folder, a nightly backup, a support-ticket attachment, and the repository that carries the site's theme.
Installing a plugin belongs to the WooCommerce guide and the seven pages beside it, and picking one belongs to the plugin overview. What follows sits under both: what a plugin archive can hold, what the Paymos one holds, and how to check the one your own gateway hands you.
What is inside a payment plugin package?
A payment plugin package is a code archive — the extension the CMS installs, and the files that describe it to the store. What it holds is the same for everyone who downloads it, and nothing in it is specific to the merchant who did.
What makes the archive worth opening is what a working integration eventually needs: an API key, an API secret, a project binding, and a webhook secret. A gateway therefore has a choice about when those exist — at build time, written into the file, or at connect time, provisioned afterwards.
Paymos makes that choice at connect time. The download route serves a static asset rather than assembling one per account, which is why the archive is identical for every merchant and carries none of those credential types.
Why would a gateway put credentials in a download at all?
A gateway writes credentials into a download to take setup work off the merchant. The premise is that the two are linked — that a store which configures nothing must have been handed something that already knew who it belonged to.
The archive pays for that shortcut. Writing a key into a plugin archive turns it from code into a bearer credential: whoever holds the file holds the account it was built for, and holding a file is a very low bar.
A baked-in key also collapses two events into one. Getting the code needs no account and is repeatable by anyone; connecting a store is authenticated and happens once. A keyed archive fuses the two.
MITRE catalogues the general form as CWE-798, Use of Hard-coded Credentials — "the product contains hard-coded credentials, such as a password or cryptographic key." Its classic case is one secret shared by every install; a per-merchant build inverts that, giving each artifact its own, and keeps the part that matters: a live secret at rest in a distributable file.
Where does a plugin archive end up after the download?
A plugin archive does not stay where it landed. The Downloads folder is the first stop of several, and none of the later ones were chosen with a credential in mind:
- The Downloads folder on a laptop, unencrypted, for as long as nobody clears it.
- A nightly site backup, and then every restore point that backup produced.
- A support-ticket attachment, the moment a merchant sends "the plugin" to whoever is helping.
- A version-controlled site repository, sitting beside the theme.
- A contractor's machine, because forwarding the file is the fastest way to hand over the work.
For a credential-free archive, all five are non-events: the file holds public bytes and discloses nothing. For an archive with a key written into it, all five are disclosures, and the merchant has no record of how many of them happened.
How do you check what your gateway is shipping you?
You check a plugin archive by unpacking it and searching it, before it reaches the site. Three commands settle the question, and none of them needs the gateway's cooperation.
# 1. unpack the archive before it reaches the store
unzip -q plugin.zip -d pkg
# 2. search it for anything shaped like a live credential
grep -rIn -iE 'api[_-]?key|secret|token|client_id|device_code' pkg
# 3. hash it, then compare with a second download of the same release
shasum -a 256 plugin.zip
The -i flag is doing work in step two: a constant is as likely to be written API_KEY or ApiKey as api_key, and a case-sensitive search walks past both. Run against the public Paymos WooCommerce source, that pattern returns 69 lines with -i and 67 without.
Read those hits as field names, labels, and translated strings rather than as findings. In that source tree thirty-eight of them sit under tests/, where fixtures like 'api_key' => 'pk_test_1234567890' are placeholders by design — a hit carrying a real value outside a test directory is the one to stop on.
The checksum settles what the search only suggests. Download the same release a second time from another machine and hash that copy too: an archive that is identical for every merchant produces one hash twice, and a package assembled per merchant cannot.
If the archive is empty, how does the plugin get its keys?
The plugin gets its keys from a single authorization. Install the release, press Connect Paymos, approve the request in the Paymos tab — and the connect step provisions the rest:
- Keys. The merchant's active Payment key is reused, or one is created when none exists, and Sandbox and Live arrive together — moving the plugin to Live later needs no reconnection.
- Webhook. The Invoice webhook is registered by the platform, reused only when callback URL, category, and project all match, and a conflicting webhook at the same URL is never silently overwritten.
- Project. Not chosen in the plugin at all — the project open in the dashboard is the one bound, and no second selector exists in the flow.
The shortcut argument therefore answers itself. A per-merchant build buys a shorter setup by putting a secret in a file; a store connected this way configured nothing either, and the file it installed still holds nothing.
Why can one archive serve every merchant?
One archive serves every merchant because a plugin is code, and code has no account inside it. The CMS ecosystems assume exactly that, and their distribution rules are written around it.
WordPress writes the assumption down as a directory rule: "The only version of the plugin that WordPress.org distributes is the one in the directory." One artifact per version, served to everyone, is the shape a CMS expects a plugin to arrive in — whichever channel delivers it.
A per-merchant build fits no such channel. That kind of build cannot be published as a release, cannot be checked against a public one, and cannot be diffed between versions, because no single published version exists to diff against.
Paymos distributes its eight official plugins as public release downloads, one package per platform per release, and the download route serves that static file rather than assembling one per account. Each release produces a single archive for WooCommerce and WHMCS, OpenCart and PrestaShop, Magento 2 and Shopware 6, CS-Cart and Easy Digital Downloads.
What does a credential-free package make possible?
A package with no credentials in it keeps three ordinary operations from becoming disclosures: sharing the file, rotating a key, and replacing the plugin. All three happen in a normal week.
Handing the file to a developer, an agency, or a colleague discloses nothing, because the Paymos archive is the file everyone else already has. The object that moves is not the object that grants access.
Rotating a credential needs no fresh download. Key and archive are separate objects, so reconnecting the store is the whole procedure after a rotation, and webhook secret rotation carries a grace period accepting signatures from both the current and the previous secret.
OWASP puts rotation in the same place: "You should regularly rotate secrets so that any stolen credentials will only work for a short time." A rotation that first requires re-downloading a plugin is a rotation that gets postponed.
Replacing the plugin is a code operation rather than a credential one. The archive can be swapped without a secret moving anywhere, because none was in it to move.
What does a credential-free archive not prove?
A credential-free archive proves one thing: the file carried no secret. It leaves two questions open, and both of them matter more than the one it answered.
Storage is the first of them. Once a credential reaches the store, something there has to keep it, and how it keeps it is a separate property with a separate answer — Paymos plugins hold those values encrypted on the store's own side, never return them to the browser, and offer no way to alter them by hand.
Runtime is the second. An archive is a snapshot of code, so reading a plugin's archive tells you what that plugin can do, not what it does on a live store with real invoices moving through it.
Storage and runtime both deserve an answer, and a checksum settles neither. The archive check is the cheap one: a few minutes of work that removes an entire class of exposure before anything is installed.
How narrow should the credential itself be?
A credential that reaches a CMS should be the narrowest one that does the job, because a store is a shared machine whose plugin folder anyone with admin access can read. The Paymos model narrows it before the question is put.
The connect step provisions a Payment credential, and Payment and Payout credentials are separate, so a checkout plugin never holds the one that moves money out. Sandbox and Production run on separate credentials against the same API contract, so the key a store tests with is not the key that takes real payments.
A Paymos credential can carry an IP allowlist of up to 50 entries, which pins it to the addresses the store runs on. A copy of that credential presented from anywhere else is rejected, so a key lifted out of a backup is not a key that works.
Merchant API authentication is HMAC-SHA256 request signing rather than a bearer token, so the secret signs a request instead of travelling inside one. API keys covers the credential model in full.
What should you ask before installing a payment plugin?
Four questions settle how a payment plugin handles credentials, and all four are answerable before the file reaches the site.
- Is the archive the same for everyone? Hash your download, fetch the same release from another machine, and compare the strings.
- Does the archive hold a live value? Search the unpacked files for credential-shaped strings; field names and fixtures are not findings.
- Where does the plugin put the credential after setup? That belongs in the documentation, not in a support reply.
- How narrow is the credential it holds? A checkout plugin carrying payout rights holds more than the job needs.
None of the four questions is a security audit, and together they rule out the failure with no recovery step: a secret already copied where nobody kept a list.
A plugin is one route from a store to a payment rail, and the other ways to accept crypto on a website covers the rest. Whichever route a store takes, its credential should begin there, never in a file that already travelled.
| Credential | Written into the download | Where it comes from instead | |
|---|---|---|---|
| API key | No | Provisioned in the connect step, Sandbox and Live together | |
| API secret | No | Provisioned in the connect step, stored encrypted by the store | |
| Project ID | No | Bound from the dashboard, not chosen in the plugin | |
| Webhook secret | No | Created with the webhook the platform registers | |
| OAuth token or device code | No | Short-lived, spent during connect and discarded |
Frequently asked questions
Does the Paymos plugin download contain my API key?
No. The archive carries no API key, API secret, project ID, webhook secret, OAuth token, or device code. Connecting the store provisions those afterwards, in a single authorization.
What do I have to configure after installing the plugin?
Nothing. Connecting is one button in the store admin and an approval in the Paymos tab; the keys, the webhook, and the project binding are all provisioned in that step.
Is the plugin file different for each merchant?
No. The downloaded package is identical for every merchant, so two downloads of the same release hold the same bytes and produce the same checksum.
Where does a Paymos plugin keep credentials after setup?
On the store's own side, encrypted, and never returned to the browser. The credential lives with the store rather than inside the distributed archive, which is why replacing the archive never moves a secret.
How do I check whether a plugin package contains credentials?
Unpack the archive and search it for credential-shaped strings, then hash the file and compare it with a second download of the same release. Field names are expected in the results; values are not, and a hit under a tests directory is a fixture.
Does downloading a plugin connect my store to Paymos?
No. Downloading moves code and nothing else. The store is connected in a separate step, by a single authorization from the store admin.
Which credential does the plugin end up holding?
A Payment credential, provisioned during connect from the merchant's active Payment key or a newly created one. Payment and Payout credentials are separate, so the plugin never holds the one that moves money out.
When NOT to use an archive inspection
- If your question is how the store keeps the key after setup, inspecting the archive answers nothing. A credential-free file says nothing about storage; read the plugin's credential-storage documentation instead.
- If the integration is your own code against the REST API rather than a packaged plugin, there is no archive to inspect. Audit the deployment path — environment variables, CI variables, config files — instead.
- If you need a credential the server itself can never read back, a plugin on your own hosting is the wrong place to look for it. Whatever a store can present to an API, the store can hold; the controls that help are the ones around the key, such as an IP allowlist.
- If the gateway offers no download at all and provisions the extension from inside its own dashboard, the checksum test has nothing to compare. Ask where the credential is written and who else can read that location.
Sources
- 1. CWE-798: Use of Hard-coded Credentials (accessed 2026-08-15)
- 2. OWASP Secrets Management Cheat Sheet (accessed 2026-08-15)
- 3. WordPress Plugin Directory guidelines (accessed 2026-08-15)
- 4. Paymos documentation — WooCommerce plugin (accessed 2026-08-15)
Last reviewed Aug 15, 2026


