Proteside Docs

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.html

The 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.

ErrorCause
409 conflictThe domain is already protected by another store, in any organization
403 plan_limitThe plan has reached its store limit (the response includes limit and plan)
400 invalid_requestInvalid 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"
}
  • target accepts domain, src (script URL) or hash; type accepts block or allow.
  • 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.pdf

Option 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 302 with a signed link valid for 1 hour; curl's -L follows the link. In other languages, follow the redirect without resending the Authorization header.
  • The maximum period is 366 days, and period_end can't be in the future.
  • format=json returns the report data; format=csv&csv_table=alerts (or scripts, headers) returns a table as CSV.
  • locale accepts pt-BR or en.
  • To receive the report automatically every week or month, schedule it with POST /reports/pci/schedule (reports:generate scope) and subscribe to the report.ready event 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"

On this page