Skip to content

Security

What protects your money, your data and your keys

Putting your despatch through us means trusting us with a balance, your customers’ addresses and a credential that can buy postage. This page describes the controls that are actually built, in enough detail that you can check them, and states plainly what is not in place yet.

We are pre-launch, and this page is written accordingly

ParcelPointPro Ltd holds no security certification, has published no penetration test and has no uptime history to quote. So none of those appear here. What appears instead is how each control works, and a section listing everything we have not done yet. Read that first if it is what you came for.

Credentials

A key we cannot show you twice

An API key can buy postage, so it is treated as money rather than as a password. We store the least we can get away with, and what we store is not enough to make a working key.

A key is a prefix followed by 64 hexadecimal characters. We keep the prefix, indexed so that authenticating a request is a single row lookup, a hash of the whole key for verification, and the last four characters so you can tell two keys apart in a list. The secret itself is never written down anywhere.

That has two consequences, and the second is not an inconvenience we intend to fix. A copy of our database yields no working keys. And if you lose your key, we genuinely cannot tell you what it was — we can only issue a new one.

A new key is shown exactly once, on the page that created it. Refreshing that page, or opening the same URL again, shows the key’s settings without the secret. Keys are created by a signed-in user of your account rather than by our staff on your behalf, so every key has a named person behind it and an entry in the audit log.

what a key looks like, and what we keep
# shown to you once, at creation
ppp_live_9f2c41ba_4d8e…64 hex characters…3b7c

# what the database holds
prefix       ppp_live_9f2c41ba   indexed — lookup is one row
secret_hash  a hash of the whole key
last_four    3b7c              so you can tell keys apart
secret       not stored, anywhere

Sandbox keys carry a different prefix, post nothing to the ledger and produce labels with no value, so a test credential can never be mistaken for a live one.

Scopes

Each key carries only the scopes it needs, such as quotes:read, shipments:write or labels:read. A key that reads tracking cannot buy a label. The scope is enforced by middleware on the route, and the published developer reference is generated from those same routes, so the documentation cannot advertise a permission the endpoint does not actually require.

Per-key rate limits

Every key has its own requests-per-minute ceiling, falling back to an account limit and then a platform default. A runaway loop in one integration cannot exhaust the budget of another, and the limit is a property of the key rather than of your IP address.

IP allowlists

A key can be pinned to the addresses your servers call from. A request carrying the correct secret from anywhere else is refused. Useful for a key that lives on a fixed despatch machine, less so for one on a laptop.

Expiry

A key can be given an expiry date when it is created and stops working the moment it passes, without anyone having to remember to go and turn it off. Contractor access is the obvious case.

Revocation

Revoking takes effect immediately and cannot be undone. Rotation is therefore create the replacement, move your systems onto it, then revoke the old one, and there is no window where you have to run without a key.

A log of what the key did

Every API request is logged with the key that made it, the endpoint, the status and the time, including requests that failed to authenticate. If a key is exposed, you can see what was done with it rather than guess.

Requests that spend money

A retry must never buy a second label

Most of what can go wrong with a shipping API is not an attacker. It is a timeout, a double-clicked button or two processes buying at the same moment. Those are handled in the same place as the security controls, because to your balance they do the same damage.

Idempotent purchases

Send an Idempotency-Key with a purchase and a retry cannot buy twice. The first request claims a unique row for that account and key; a retry arriving after it finished replays the stored response exactly, and one arriving while it is still running gets a 409 rather than a guess. Reusing a key with a different body is refused outright — quietly returning the earlier shipment would be worse than an error. Failed requests are not remembered, so a purchase that did not happen can be retried with the same key.

Charged first, bought second

We take the money from your balance, inside a lock on it, and buy the label afterwards. If the carrier then fails, the charge is reversed and you keep your money. The other order risks a label we have paid the carrier for and cannot collect on, and that risk would end up priced into what you pay.

Locked against concurrency

A charge takes a row lock on the funding account before it reads the balance, so two bulk purchases running at the same instant cannot both spend the same money. There is a test that forks real processes to prove it, because a bug of this shape does not show up under sequential testing.

Double-entry ledger

Every movement of money is a balanced double-entry record in integer pence — no floats, no rounding drift. Balances are derived by summing the ledger rather than read from a stored total that could quietly diverge from it, and the database itself refuses to commit an entry that does not balance.

Corrections, not edits

Ledger entries are never edited or deleted. A mistake is corrected by posting a reversal, so the original and the correction both appear on your statement. Anything else would mean a history that changes after you have read it.

Audit trail

Money movements and administrative actions are written to an activity log: who did it, what changed and when. That covers our staff as well as yours, which is the point of having it.

Webhooks

Signed so an old delivery cannot come back

A webhook is a request that arrives at your server claiming to be from us. Verifying it properly is a shared job, so here is exactly what we send and what your end should check.

Every delivery carries an X-PPP-Signature header holding a timestamp and an HMAC-SHA256 digest. The timestamp sits inside the signed string, not merely beside it. If it were only a header, yesterday’s body could be replayed with today’s timestamp and the signature would still verify.

We sign the exact bytes we put on the wire, not a re-encoded object. Verify against the raw request body before your framework parses it: two JSON encoders will not agree on key order, unicode escaping or float formatting, and that mismatch is the usual reason signature checks fail intermittently. Compare digests with a constant-time function, since an ordinary string comparison on a hex digest leaks through timing how many leading characters a guess got right.

Reject anything outside a five-minute window, which is the tolerance our own reference verifier uses. Deliveries that fail are retried on a fixed backoff of ten seconds, one minute, five minutes, thirty minutes and two hours, so a receiver that is briefly down does not lose events.

the signature scheme
X-PPP-Signature: t=1785312000,v1=8f3c…

# what we signed
signed = "{t}.{raw request body}"
v1     = hmac_sha256(signed, your endpoint secret)

# what your end should do
1. read the RAW body, before any JSON parsing
2. refuse if |now - t| > 300 seconds
3. recompute v1 and compare in constant time

Working verification code, in the same shape, is in the developer reference.

The URL you give us

Guarding our own outbound requests

A webhook endpoint is a URL you type in and our server then fetches. That is server-side request forgery by design, and its whole value to an attacker is what our server can reach that they cannot: cloud instance metadata, a queue on an internal network, an admin interface on a private address.

So before we accept an endpoint, and again immediately before every delivery, we resolve the host and refuse private, loopback and link-local addresses. Endpoints must use https, since a signature proves who sent a payload but does nothing to keep it private in transit, and a URL carrying a username and password is refused because those end up in logs. Redirects are not followed, so a 302 to a private address cannot be used to step around the check.

An honest limit. DNS can still change between our check and the HTTP client’s own lookup. Re-checking before each delivery narrows that window from indefinite to milliseconds; it does not close it. Closing it properly means pinning the resolved address into the connection, which our HTTP client does not currently expose.

People and access

Who can reach your account, including us

Tenant isolation and staff access are the two things a customer cannot verify from the outside, so they are described here in the same detail we would want if we were reading this page about someone else.

Two-factor authentication

Time-based one-time codes from any standard authenticator app. It is mandatory for our own staff — nobody reaches the admin panel without it — and available to every user on your account. It is not forced on your team because that is your decision to make rather than ours, but we would rather you turned it on.

Tenant isolation

Your account is the boundary. Every tenant-owned record carries an account, a global query scope applies it to every read, and the account in scope comes from exactly one place: the signed-in tenant in the portal, the API key on an API request. It is covered by tests rather than by convention.

Resolved after authentication, never before

Nothing on the API is resolved from the URL before the key has been authenticated. Framework-level model binding runs earlier than route middleware, which means a record could be fetched before the account context existed — precisely how one account can be handed another account shipment. Our controllers look references up themselves, afterwards.

Roles per account, not globally

Owner, Manager, Operator, Finance and Read-only are set up on each account and apply only to that account. A user who belongs to two accounts, as third-party logistics operators and agents do, carries different permissions in each and cannot carry one across to the other.

Separate sessions for staff and customers

The admin panel and the customer portal use different session cookies. Signing into one does not sign you into the other, and a member of our staff cannot end up holding a customer session by accident.

Support sessions are visible and logged

When our staff need to see what you see, they start a support session. It is handed over with a single-use token that expires in sixty seconds, cannot be nested inside another, shows a banner for the whole time it runs, and is written to the activity log at both the start and the end. The staff member's own session is never replaced by yours.

Status gates spending, not access

A suspended account can still sign in to read its history and settle what it owes; only an active account can spend or use the API. A closed account is locked out entirely. Separating those means we never have to lock someone out of their own records to stop a spend.

API access is granted, not switched on

API access is enabled by us on request rather than being self-service, and a new account starts with no API access, an empty balance and no credit limit. It cannot spend anything at all until it has been funded, which is what makes open registration safe.

What we hold

Secrets at rest, and labels that are worth money

Two categories deserve saying out loud: the credentials we hold with the carriers, and the label files themselves. A label is postage we have already paid for, so anyone holding the file can print it.

Carrier credentials encrypted at rest

The credentials that let us buy from each carrier are encrypted in the database and are never displayed again after they have been saved, including to our own staff. Replacing one means entering a new value, not reading the old one.

Secrets redacted from logs

Every carrier API call is logged, and credentials are stripped from those logs including the error messages — a 401 is exactly the moment a carrier tends to echo a submitted key back at you. The redaction list is derived from each carrier definition, so a new carrier cannot be connected without its secrets being redacted too.

Labels behind signed and authenticated links

Label files sit on a private disk, never a public one, and the download route requires a valid signature and a signed-in user belonging to the owning account, and checks the label belongs to the shipment named in the URL. A signature alone is not enough because signed links get forwarded, pasted into chats and logged by proxies. Every print is counted and recorded.

Passwords stored as one-way hashes

Passwords are stored only as a hash. We cannot read yours, we will never email it to you, and nobody at ParcelPointPro will ever ask you for it.

No card data at all

We hold no card numbers, because there is no card payment gateway yet — balances are topped up by bank transfer and recorded against your account by a member of our staff. When card payments do arrive they will run through a payment provider, and card numbers will not touch our servers then either.

UK data residency is the intent

We intend to keep customer data in the United Kingdom and to name our hosting providers and sub-processors in the privacy notice before launch. That is an intention stated plainly, not a certification, and the production environment is not running yet.

What we collect, why we hold it and how long we keep it is set out in the privacy notice, and your obligations and ours are in the terms of service. Both are drafts pending legal review and are marked as such.

Stated plainly

What is not in place yet

A security page that only lists strengths is a sales page. This is the other half, and it is the part worth reading if you are deciding whether to depend on us.

No security certification

We do not hold ISO 27001, SOC 2, PCI DSS or Cyber Essentials, and you should not accept a claim from us that we do. If a certification is a procurement requirement on your side, tell us which one and we will tell you honestly where we are rather than what you would like to hear.

No published penetration test

No independent penetration test has been carried out or published. When one has been, we will say who did it, when, and what came out of it.

No production track record

The platform is pre-launch. There is no uptime history to quote, so we do not quote one, and there is no availability figure, response-time target or support-hours commitment anywhere on this page. Those belong in a contract once we can actually meet them.

No live carrier connections yet

Only the sandbox is operating. The four carrier integrations are built, but each one deliberately refuses to quote or buy until it has been verified against the carrier’s own endpoint, because a plausible guess would sell you carriage we cannot deliver.

Top-ups are not automatic

There is no payment gateway. You pay by bank transfer and a member of our staff records it against your balance, which is why we hold no card data and also why a top-up is not instant.

No bug bounty

We do not run one and are not going to imply otherwise. Reports are still genuinely welcome, and how to send one is immediately below.

Terms and privacy are drafts

Both documents are published so that you can read them before signing up, but both are pending review by our solicitors and neither is final.

No named security team

We are small and pre-launch. Reports go to the address below and are read by the people who build the platform, not by a dedicated security function that does not exist yet.

This list will get shorter. When something moves off it, it moves onto the sections above with the same amount of detail — and if you are weighing us up against a supplier whose security page has no list like this one, it is worth asking them what theirs would say.

Responsible disclosure

Found something? Tell us.

If you have found a way to reach data that is not yours, spend money that is not yours, or get past any of the controls described above, we want to hear about it before anyone else does.

Email support@parcelpointpro.com with “Security” in the subject line. Plain text is fine. A short, clear report beats a long automated one.

What to include

  • What you found, in a sentence, and the URL or endpoint it affects.
  • The steps to reproduce it, in the order you did them.
  • What someone could actually do with it — read another customer’s data, spend a balance, mint a key.
  • Whether you would like to be credited when it is fixed, and under what name.

What we ask of you

  • Test against your own account or the sandbox, never against another customer.
  • Do not access, change, download or keep anyone else’s data. If you reach something you should not, stop and describe how far you got.
  • No denial-of-service testing, no automated scanners pointed at production, no social engineering of our staff or our carriers.
  • Give us a reasonable chance to fix the issue before you publish it.

What we undertake

  • We will read it, reply to say what we make of it, and tell you what we intend to do about it.
  • We will keep you informed while it is open, and tell you when it is fixed.
  • We will credit you by name if you would like us to, and stay quiet about you if you would not.
  • We will not pursue legal action against anyone acting in good faith within the lines above, and we will not report you to your employer or your provider for it.

What we are deliberately not promising: a fixed response time, and a payment. We would rather leave both out than publish a target we have not yet proved we can meet. If you are unsure whether something is in scope, ask before you test it.

Questions your security review needs answered

Send us the questionnaire, the clause or the single awkward question. We will answer it as it stands today, including where the answer is that we have not built that yet.