Recipes
End-to-end curl flows to register stores, review scripts, create rules, handle alerts, download PCI DSS reports and set up webhooks.
Each recipe is an end-to-end flow you can copy and adapt. The examples use curl and jq and these variables:
export B="https://app.proteside.com/api/v1"
export PROTESIDE_TOKEN="ps_live_Ab3dEf6hIj9kLm2nOp5qRs8t_..."The IDs and response data are fictitious. Responses are abbreviated: "..." marks omitted fields.
Create a store and get the SDK snippet
Scopes: stores:write and stores:read.
Create the store
Provide the domain, the payment methods and, optionally, the payment pages and the receiving keys. The domain's
scheme (https://), path and port are discarded.
curl -s -X POST "$B/stores" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"domain": "https://www.mystore.com/",
"name": "My Store",
"payment_methods": ["pix", "card"],
"expected_iframe_origins": ["js.stripe.com"],
"recipients": [{ "method": "pix", "key": "12.345.678/0001-90", "label": "Main CNPJ" }],
"payment_pages": [{ "url_pattern": "/checkout/*", "payment_methods": ["pix", "card"] }]
}' | tee store.json | jq '{id, domain, sdk_key, safe_slug}'{
"id": "0c1d2e3f-1111-4222-8333-444455556666",
"domain": "mystore.com",
"sdk_key": "pk_live_3f9c0a1b2c3d4e5f60718293a4b5c6d7",
"safe_slug": "mystore-ps3fa9c1"
}The full response (201) also includes sdk_config, recipients (the Pix key comes back masked, as
**.345.678/****-**; the plaintext key isn't stored), pages and snippets with the installation code for HTML,
Next.js, Nuxt and WordPress. The store also gets an "E-mail (padrão)" channel (the default email channel) for the
organization owner.
Save the snippet
jq -r '.snippets.html' store.json > proteside-snippet.htmlThe snippet has two parts, between <!-- Proteside Start --> and <!-- Proteside End -->: an inline script (the
bootstrapper) and shield.js loaded from the CDN with data-key set to the sdk_key. Paste the whole block as the
first item in the checkout page's <head>, following the installation guide.
Check the installation
After publishing the snippet and opening the checkout, check the status:
STORE=$(jq -r .id store.json)
curl -s "$B/stores/$STORE/status" -H "Authorization: Bearer $PROTESIDE_TOKEN"{
"id": "0c1d2e3f-1111-4222-8333-444455556666",
"domain": "mystore.com",
"status": "active",
"installed": true,
"last_event_at": "2026-10-06T12:20:12.000Z",
"entitled": true,
"paused": false,
"channel": "stable",
"mode": "monitor",
"developer_mode": false
}installed becomes true when the SDK has sent any event in the last 24 hours.
| Error | Cause |
|---|---|
409 conflict | The domain is already protected by another store, in any organization |
403 plan_limit | The plan has reached its store limit (the response includes limit and plan) |
400 invalid_request | Invalid domain or unknown field |
To rotate the SDK key later, use POST /stores/{id}/rotate-key: the response includes the new sdk_key and the
snippets, and the old key stays valid for 24 hours (previous_key_valid_until).
Review pending scripts and authorize with a justification
Scopes: scripts:read and scripts:review. PCI DSS 4.0, requirement 6.4.3, requires every script on the payment
page to be authorized with a justification. The API records the justification, the date and the author
(api:<prefix>).
List what's awaiting review
curl -s "$B/scripts?store_id=$STORE&status=needs_review&limit=100" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
| jq '.data[] | {id, src, type, category, first_seen_at}'{
"id": "b2c3d4e5-6666-4777-8888-999900001111",
"src": "https://www.googletagmanager.com/gtm.js",
"type": "known_third",
"category": "tag_manager",
"first_seen_at": "2026-10-05T18:02:44.000Z"
}Authorize with a justification
justification is required (10 to 2000 characters). expires_days (1 to 3650) sets when the authorization expires
and the script goes back to review.
SCRIPT=b2c3d4e5-6666-4777-8888-999900001111
curl -s -X POST "$B/scripts/$SCRIPT/authorize" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"justification": "Marketing GTM container; does not access payment fields. Approved in ticket SEC-142.",
"expires_days": 90
}'{
"id": "b2c3d4e5-6666-4777-8888-999900001111",
"src": "https://www.googletagmanager.com/gtm.js",
"status": "authorized",
"authorization_method": "api",
"authorized_by": "api:ps_live_Ab3dEf6hIj9kLm2nOp5qRs8t",
"justification": "Marketing GTM container; does not access payment fields. Approved in ticket SEC-142.",
"reviewed_at": "2026-10-06T12:00:00.000Z",
"expires_at": "2027-01-04T12:00:00.000Z",
"review_due_at": "2027-01-04T12:00:00.000Z",
"...": "...",
"block_rules_deactivated": false
}When you authorize, any active blocking rules that match the script (by domain, URL or hash) are deactivated.
Differences from the spec
Scripts authorized through the API get authorization_method: "api" (the spec says manual and doesn't list api
among the possible values). The block_rules_deactivated field is a boolean that indicates whether any rule was
deactivated, not a count. If you generated a client from the spec, accept these values.
To block instead of authorize, use POST /scripts/{id}/block with an optional justification. Proteside creates the
blocking rule on its own: by hash, for an inline script, or by domain, for an external script. The rule comes back in
rule in the response.
Create a domain blocking rule
Scope: rules:write (and scripts:read to list rules).
curl -s -X POST "$B/rules" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"store_id": "'"$STORE"'",
"type": "block",
"target": "domain",
"value": "cdn-skimmer.example",
"label": "Known skimmer"
}'{
"id": "4d5e6f70-aaaa-4bbb-8ccc-dddd00001111",
"store_id": "0c1d2e3f-1111-4222-8333-444455556666",
"type": "block",
"target": "domain",
"value": "cdn-skimmer.example",
"label": "Known skimmer",
"active": true,
"created_at": "2026-10-06T12:01:00.000Z"
}targetacceptsdomain,src(script URL) orhash;typeacceptsblockorallow.- If an identical rule already exists (same store, type, target and value), it's reactivated instead of duplicated.
- Active rules reach the SDK in the store's configuration.
To deactivate without deleting:
curl -s -X PATCH "$B/rules/4d5e6f70-aaaa-4bbb-8ccc-dddd00001111" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"active": false}'To delete: DELETE $B/rules/{id}, which responds {"id": "...", "deleted": true}.
List open alerts and resolve them
Scopes: alerts:read and alerts:write.
curl -s "$B/alerts?store_id=$STORE&status=open&severity=critical&since=2026-10-01T00:00:00Z" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
| jq '.data[] | {id, type, severity, page, occurrences, last_seen_at}'{
"id": "e1f2a3b4-8888-4999-8000-111122223333",
"type": "PIX_TAMPERED",
"severity": "critical",
"page": "/checkout/payment",
"occurrences": 3,
"last_seen_at": "2026-10-06T09:52:10.000Z"
}since filters by the alert's most recent occurrence. After investigating, resolve it with a note:
curl -s -X PATCH "$B/alerts/e1f2a3b4-8888-4999-8000-111122223333" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "resolved", "note": "False positive: new Pix key registered by the finance team."}'{
"id": "e1f2a3b4-8888-4999-8000-111122223333",
"type": "PIX_TAMPERED",
"severity": "critical",
"status": "resolved",
"resolved_at": "2026-10-06T12:02:00.000Z",
"resolved_by_label": "api:ps_live_Ab3dEf6hIj9kLm2nOp5qRs8t",
"note": "False positive: new Pix key registered by the finance team.",
"...": "..."
}To reopen, send {"status": "open"}. If there's already another open alert of the same type and source, the response
is 409 conflict.
Resolving an alert doesn't fire the alert.resolved webhook. If other systems need to know, notify them from your
own integration.
Download the PCI DSS report as a PDF
Scope: reports:read.
Without dates, every call generates a new report
GET /reports/pci without period_start and period_end uses "the last 7 days up to now". Since "now" changes on
every call, Proteside generates a new report each time, which is slow and fills up the history. For recurring
reports, list the ones that already exist; for a specific period, send fixed dates.
Option 1: download a report that already exists. List the store's reports and download the PDF by id:
curl -s "$B/reports/pci/snapshots?store_id=$STORE&kind=weekly&limit=1" \
-H "Authorization: Bearer $PROTESIDE_TOKEN"{
"data": [
{
"id": "5a6b7c8d-9999-4aaa-8bbb-cccc0000dddd",
"store_id": "0c1d2e3f-1111-4222-8333-444455556666",
"kind": "weekly",
"period_start": "2026-09-28T09:00:00.000Z",
"period_end": "2026-10-05T09:00:00.000Z",
"locale": "pt-BR",
"schema_version": 2,
"sha256": "9b74c9897bac770ffc029102a200c5de3e2b4c1f0a8d7e6c5b4a392817060504",
"generated_by": "cron:weekly-reports",
"has_pdf": true,
"csv_tables": ["scripts", "headers", "alerts"],
"created_at": "2026-10-05T09:02:11.000Z"
}
],
"next_cursor": "WyIyMDI2LTEwLTA1VDA5OjAyOjExLjAwMFoiLCI1YTZiN2M4ZC05OTk5LTRhYWEtOGJiYi1jY2NjMDAwMGRkZGQiXQ"
}curl -sL "$B/reports/pci/snapshots/5a6b7c8d-9999-4aaa-8bbb-cccc0000dddd?format=pdf" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
-o pci-2026-09-28.pdfOption 2: generate the report for a closed period. Send fixed dates, preferably whole days. Repeating the same call reuses the report that was already generated:
curl -sL "$B/reports/pci?store_id=$STORE&period_start=2026-09-01T00:00:00Z&period_end=2026-10-01T00:00:00Z&format=pdf&locale=pt-BR" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
-o pci-2026-09.pdf- For PDF, the API responds
302with a signed link valid for 1 hour;curl's-Lfollows the link. In other languages, follow the redirect without resending theAuthorizationheader. - The maximum period is 366 days, and
period_endcan't be in the future. format=jsonreturns the report data;format=csv&csv_table=alerts(orscripts,headers) returns a table as CSV.localeacceptspt-BRoren.- To receive the report automatically every week or month, schedule it with
POST /reports/pci/schedule(reports:generatescope) and subscribe to thereport.readyevent on a webhook.
Create a webhook and send a test
Scope: webhooks:manage.
Create the webhook
curl -s -X POST "$B/webhooks" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://ops.mystore.com/hooks/proteside",
"events": ["alert.created", "integrity.changed", "sdk.silent", "report.ready"],
"min_severity": "medium"
}' | tee webhook.json | jq '{id, has_secret, secret}'{
"id": "a9b8c7d6-5555-4666-8777-888899990000",
"has_secret": true,
"secret": "whsec_9f8e7d6c5b4a39281706f5e4d3c2b1a0"
}Store the secret in the secrets vault of the service that receives the webhook: it won't be shown again. We use
min_severity: "medium" so we also receive sdk.silent.
Send the test
WH=$(jq -r .id webhook.json)
curl -s -X POST "$B/webhooks/$WH/test" -H "Authorization: Bearer $PROTESIDE_TOKEN"{ "id": "a9b8c7d6-5555-4666-8777-888899990000", "delivered": true, "error": null }The response is always 200. If delivered is false, the reason is in error (for example http_401: ... when
your endpoint rejected the signature, or timeout).
Validate the signature on the receiver
Implement the X-Proteside-Signature check as shown in
Verify the signature and repeat the test until delivered is true.
Partner: create a store in a child organization
Scopes: orgs:manage, stores:write, stores:read and webhooks:manage. See Partners for the
full model.
Create the child organization
curl -s -X POST "$B/organizations" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name": "My Store", "owner_email": "you@mystore.com", "plan": "growth"}' \
| tee child.json | jq '{id, billing_mode, owner_invite}'{ "id": "11112222-3333-4444-8555-666677778888", "billing_mode": "partner", "owner_invite": "sent" }Create the store in the child
Pass org_id in the query string. Use a new Idempotency-Key: the key applies to the token's organization, not to
the child.
CHILD=$(jq -r .id child.json)
curl -s -X POST "$B/stores?org_id=$CHILD" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"domain": "mystore.com", "payment_methods": ["pix", "card"]}' \
| tee child-store.json | jq '{id, org_id, sdk_key}'{
"id": "0c1d2e3f-1111-4222-8333-444455556666",
"org_id": "11112222-3333-4444-8555-666677778888",
"sdk_key": "pk_live_3f9c0a1b2c3d4e5f60718293a4b5c6d7"
}Hand the snippet in .snippets.html to your client (or install it yourself).
Create the child's webhook
A child's events only go to webhooks created in it:
curl -s -X POST "$B/webhooks?org_id=$CHILD" \
-H "Authorization: Bearer $PROTESIDE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://ops.parceiro.example/hooks/proteside", "min_severity": "medium"}' \
| jq '{id, secret}'Track installation and usage
STORE=$(jq -r .id child-store.json)
curl -s "$B/stores/$STORE/status?org_id=$CHILD" -H "Authorization: Bearer $PROTESIDE_TOKEN"
curl -s "$B/organizations/$CHILD/usage" -H "Authorization: Bearer $PROTESIDE_TOKEN"