Last updated 6 August 2026. This page answers, field by field, why each protected customer data field is requested and where it is used. Each field has its own anchor: #email, #phone, #name, #address.
What the app does. Paid Signal sends a server-side conversion event to Meta, TikTok, Snapchat and Google Ads at the moment a Shopify order is confirmed paid — not when it is placed. In cash-on-delivery and refund-heavy markets those are very different sets of orders, and optimising ad delivery on the first set wastes budget on people who never pay.
Why these fields exist at all. A server-side conversion carries no cookie and no browser. The only way an ad platform can attribute it to the person who saw the ad is a set of hashed identifiers supplied in the request — this is the mechanism Meta calls Advanced Matching, and TikTok, Snapchat and Google Ads each require the same thing in the same format. Without them the event is delivered and matched to nobody, which is the same as not sending it.
The design decision that governs every field below
Each identity field is SHA-256 hashed by the webhook handler that receives it, and only the digest is written. Paid Signal has no plaintext column for email, phone, name, city or country — not as a policy, but structurally: those columns do not exist in the schema. The values cannot be read back by us, by an administrator, or by anyone who obtained the database, because they were never stored. A hash is exactly the format the ad platforms require, so nothing is lost by never holding the original.
| Field | Used for | Stored as | Plaintext kept |
|---|---|---|---|
| Conversion matching on all four ad platforms; payer audience membership | em_hash | No | |
| Phone | Conversion matching on Meta, TikTok, Snapchat — the primary identifier in COD markets | ph_hash | No |
| Name | Meta Advanced Matching (fn, ln) | fn_hash, ln_hash | No |
| Address | Meta Advanced Matching (ct, country) — city and country only | ct_hash, country_hash | No |
| Read from | The order’s email, falling back to customer.email, on the orders/create, orders/updated and orders/paid webhooks. |
| What happens to it | Trimmed, lowercased, then SHA-256 — the normalisation every ad platform specifies. The plaintext exists only as a local variable inside the request handler and is never assigned to a column. |
| What is stored | dcp.orders.em_hash — a 64-character lowercase hex digest. |
| Where it is used | Meta Conversions API as user_data.em; TikTok Events API as user.email; Snapchat Conversions API as user.em; Google Ads enhanced conversions as hashedEmail. It is also the membership key for the payer audiences the app maintains on Meta and Snapchat. |
| Why the app needs it | This is the single highest-matching identifier across all four platforms. Without it, a paid-order conversion arrives at the platform attached to no person, cannot be credited to the campaign that produced the sale, and cannot be used to build a lookalike audience — which is the entire function of the app. |
| Read from | The order’s phone, falling back to shipping_address.phone then customer.phone, on the same order webhooks. |
| What happens to it | Reduced to digits only, normalised to full international form (no +, no spaces, no leading zero), then SHA-256 — the format the platform APIs require. Plaintext is never written. |
| What is stored | dcp.orders.ph_hash. |
| Where it is used | Meta as user_data.ph; TikTok as user.phone; Snapchat as user.ph. It is the fallback membership key for payer audiences when a customer has no email on the order. |
| Why the app needs it | Paid Signal serves cash-on-delivery markets, where a large share of orders are placed with a phone number and no email address at all. For those orders the phone hash is the only identifier available, and dropping it would make the app silently useless for exactly the merchants it is built for. |
| Read from | shipping_address.first_name / last_name, falling back to the customer record, on the same order webhooks. |
| What happens to it | Lowercased with punctuation and whitespace removed, then SHA-256 — Meta’s specified normalisation for name parameters. Plaintext is never written. |
| What is stored | dcp.orders.fn_hash and ln_hash. |
| Where it is used | Meta Conversions API as user_data.fn and user_data.ln. Not sent to TikTok, Snapchat or Google Ads, because those APIs do not use it for this event type. |
| Why the app needs it | Meta matches a server event on the combination of identifiers supplied, and each additional parameter raises the match rate. For COD orders — frequently a phone number and a name and nothing else — name is often the difference between an event that matches a person and one that is discarded. A discarded event is indistinguishable, to the merchant, from the app not working. |
| Read from | shipping_address.city and shipping_address.country_code, on the same order webhooks. |
| What happens to it | City: lowercased, punctuation and whitespace removed, then SHA-256. Country: the two-letter ISO code, lowercased, then SHA-256. Both are Meta’s specified normalisations. Plaintext is never written. |
| What is stored | dcp.orders.ct_hash and country_hash. |
| Where it is used | Meta Conversions API as user_data.ct and user_data.country. Not sent to any other platform. |
| Why the app needs it | Same mechanism as name: two more matching parameters on a server event that has no cookie to fall back on. City and country are also what make the event plausible to Meta’s matching for a market where many customers share common names. |
| What we do not read | Street address, apartment, postal code, province and company are never read, never hashed and never stored. The Address scope grants access to them; the app deliberately takes only the two fields it can use. No column exists for the rest. |
For completeness, this is the entire identity surface of the orders table. Every column below holds a SHA-256 digest and nothing else:
em_hash · ph_hash · fn_hash · ln_hash · ct_hash · country_hash · external_id_hash
Alongside them the app stores order facts (order id and number, value, currency, payment status, gateway, and the times the order was placed and paid) and any advertising click identifiers already present on the order (fbc, fbp, gclid, ttclid, sccid), which are advertising identifiers rather than customer identity.
The delivery ledger records what was sent to each platform, and it records the shape of the request, never its values: for each attempt it stores the list of which matching keys were present — for example ["ct","country","em","fn","ln","ph"] — and never the hashes themselves. An engineer reading the audit trail can confirm what was sent without being able to see who it was about.
Paid Signal implements all three of Shopify’s mandatory compliance webhooks, with HMAC verification:
customers/redact — every hash belonging to that customer is
erased from the orders table, from the event ledger and from the identity index.
The customer is also removed from the payer audiences at Meta and Snapchat, so
deletion propagates outside our own system rather than stopping at our database.
shop/redact — the workspace and all its data are erased.
customers/data_request — answered with what is held, which for
identity is a set of irreversible digests.
Independently of any request, a scheduled sweeper strips identity from orders older than the workspace’s retention window (365 days by default) while keeping the aggregate order facts the merchant’s reporting depends on.
Deleting order rows is otherwise impossible by construction: the ledger is append-only and enforced by a database trigger. The compliance webhooks are the only code path in the application permitted to remove identity, and they must open that door explicitly to do it.
Every table is protected by PostgreSQL row-level security with FORCE enabled, and each query runs inside a transaction that sets the acting workspace. One merchant’s data is unreadable from another merchant’s session as a database guarantee, not as an application convention. The application refuses to start if row-level security is not in force.
info@paidsignal.app — see also the privacy policy.