SKAdNetwork S2S Workflows: How to Verify Ad Network Postbacks

opoinstall
2026-08-21
5 min read

How do DSPs handle SKAdNetwork postbacks? Demand-Side Platforms (DSPs) and ad networks handle SKAdNetwork postbacks by establishing secure HTTP POST ingestion endpoints, constructing the serialized UTF-8 message string using the U+2063 delimiter, verifying Apple’s cryptographic ECDSA P-256 signature against Apple’s published public key, and recording verified transaction IDs to prevent duplicate processing before updating bidding models.

An SKAdNetwork install-validation postback is an Apple-signed HTTPS POST notification that the operating system sends to an eligible ad network and, for winning attributions, optionally to the advertised app developer’s configured copy endpoint. To ensure data integrity, backend ingestion systems must verify Apple’s ECDSA P-256 signature, validate parameter serialization, and enforce transaction-level deduplication.

Term Definition
SKAdNetwork Apple’s platform-level framework for privacy-preserving ad campaign attribution.
Install-Validation Postback An Apple-signed JSON payload containing install-validation and attribution metadata after an eligible ad conversion.
ECDSA P-256 The elliptic curve cryptographic algorithm used by Apple to sign install-validation postbacks.
Transaction ID A unique validation identifier that receivers use as the idempotency key for duplicate detection.

The Architecture of SKAdNetwork Postback Ingestion for DSPs and Ad Networks

The Dual Ingestion Pipeline: Direct Ad Network Delivery vs Developer Postback Endpoints

When an attributed iOS application install occurs, Apple’s attribution subsystem dispatches install-validation postbacks over HTTPS POST:

  • Ad Network Ingestion: The device delivers the primary winning postback (did-win: true) directly to the server URL registered under the corresponding ad-network-id in Apple’s registry.
  • Developer Copy Ingestion: If the advertised app specifies the NSAdvertisingAttributionReportEndpoint key in its Info.plist, the device concurrently dispatches an exact copy of the winning postback directly to the developer’s server.
  • Nonwinning Postback Routing: Starting in SKAdNetwork 3.0, if multiple ad networks qualified for the attribution but did not win, the device sends up to five nonwinning postbacks (did-win: false) directly to those secondary qualifying ad networks. Nonwinning postbacks are not delivered to the developer copy endpoint.

Backend ingestion endpoints should respond with HTTP 200 OK. If the device does not receive a 200 response, it may retry delivery up to nine times over a maximum of nine days.

SKAdNetwork postback ingestion and verification workflow

The Role of NSAdvertisingAttributionReportEndpoint in Developer Auditing

The NSAdvertisingAttributionReportEndpoint enables app developers to receive direct copies of winning postbacks independently of ad network forwarding:

  • Independent Auditing: Developers receive exact copies of all winning postbacks generated for their application, enabling internal validation of ad network reporting.
  • Dedicated Endpoint Path: The developer server must host the endpoint at https://<domain>/.well-known/skadnetwork/report-attribution/.
  • AdAttributionKit Distinction: For AdAttributionKit, Apple defines a separate configuration routing to https://<domain>/.well-known/appattribution/report-attribution/, which utilizes a JSON Web Signature (JWS) verification architecture.

How MMPs Ingest, Aggregate, and Normalize Multi-Network S2S Event Streams

Depending on commercial integrations, Mobile Measurement Partners (MMPs) may ingest SKAdNetwork data through developer-side forwarding, ad-network integrations, or custom partner server flows:

  • Multi-Source Ingestion: Ingesting verified postback data forwarded from developer endpoints alongside direct ad network reporting streams.
  • Deduplication Across Streams: Normalizing and deduplicating records using the unique transaction-id across shared ad network and developer copies.
  • Downstream BI Normalization: Mapping coarse-grained and fine-grained conversion values to client-defined revenue models and funnel events.

See Also: SKAdNetwork ──> Mobile Attribution Model

Cryptographic Verification: Validating Apple’s ECDSA P-256 Signature

Understanding the Cryptographic Stack: NIST Curve P-256 (secp256r1) with SHA-256

Every SKAdNetwork postback includes an attribution-signature field. This cryptographic signature is generated by Apple using the Elliptic Curve Digital Signature Algorithm (ECDSA) with the NIST P-256 (secp256r1) curve and a SHA-256 digest.

The signature validates two fundamental security properties:

  • Authenticity: The postback was generated directly by Apple’s platform subsystem on a verified device, not forged by an adversarial client or proxy.
  • Integrity: The parameters covered by the signature have not been altered in transit.

Using Apple’s Published SKAdNetwork Public Key

To verify the signature, the ingestion server must load Apple’s official public key. For SKAdNetwork 2.1 and later, Apple publishes a dedicated NIST P-256 public key in its developer documentation:

  • Key Initialization: The public key is loaded into memory as a standard X.509/DER public key object during server initialization.
  • Asymmetric Signature Check: The verification engine reconstructs the exact UTF-8 serialized message string, computes the SHA-256 hash, and verifies the Base64-decoded attribution-signature against the reconstructed message.

[Device / Subsystem] ──► [Dispatches Signed JSON Postback]
                                     │
                                     ▼
                  [DSP / Ad Network Ingestion Endpoint]
                  (HTTPS POST to registered postback URL)
                                     │
                                     ▼
                  [Parse JSON & Reconstruct Message String]
                  (Concatenate UTF-8 fields with \u2063)
                                     │
                                     ▼
                  [ECDSA P-256 Public Key Signature Verification]
                                     │
                      ┌──────────────┴──────────────┐
                      ▼                                                          ▼
               [Signature Valid]                                         [Signature Invalid]
                      │                                                          │
                      ▼                                                          ▼
            [Atomic Deduplication]                                       [Log Error & Discard]
            (Check transaction-id)
                      │
                      ▼
            [Process Attribution]
SKAdNetwork ECDSA P 256 signature verification flow

Why a Hash Alone Is Insufficient: Asymmetric Signature Verification

Because Apple signs the payload using its private key and does not distribute a shared secret, symmetric validation (such as HMAC-SHA256) cannot be used. Ingestion engines must implement standard asymmetric public-key signature verification using standard cryptographic libraries (such as OpenSSL, Node.js crypto, or Python cryptography).

Constructing the Message String for Signature Verification

The Strict Serialization Protocol: The Role of the Invisible Separator (\u2063)

Apple specifies an exact UTF-8 byte serialization format to construct the message string for signature validation. The parameters must be concatenated in a precise sequence, separated by the invisible Unicode character \u2063 (U+2063 Invisible Separator, UTF-8 byte sequence 0xE2 0x81 0xA3):

Message=Param1    "2˘063"    Param2    "2˘063"  Paramn\text{Message} = \text{Param}_1 \;\|\; \text{"\u2063"} \;\|\; \text{Param}_2 \;\|\; \text{"\u2063"} \dots \|\; \text{Param}_n

Substituting whitespace, standard punctuation, or alternative Unicode separators will result in cryptographic verification failure.

Version-Specific Parameter Ordering for SKAN 4.0

According to Apple Developer Documentation on Verifying an Install-Validation Postback, the parameters for SKAdNetwork 4.0 postbacks must be serialized in the following exact order:

  1. version (e.g., "4.0")
  2. ad-network-id (e.g., "example123.skadnetwork")
  3. source-identifier (e.g., "4821")
  4. app-id (e.g., 1234567890)
  5. transaction-id (e.g., "6a8b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d")
  6. redownload (e.g., "true" or "false" as a lowercase string)
  7. source-app-id (for app-to-app ads) OR source-domain (for web-to-app ads in Safari), included only if present in the postback
  8. fidelity-type (e.g., 1 for StoreKit-rendered ads or SKAdNetwork-attributed web ads; 0 for view-through ads)
  9. did-win (e.g., "true" or "false" as a lowercase string)
  10. postback-sequence-index (e.g., 0, 1, or 2)

Crucial SKAN 4 Specification: Conversion Values Are Excluded from the Signature

In SKAdNetwork 4.0, Apple’s signature does not include conversion-value or coarse-conversion-value, even when one of those fields is present in the JSON payload. The serialized string for SKAN 4 ends with postback-sequence-index. Attempting to append conversion values to the message string will cause verification to fail.

The JSON payload below illustrates a complete SKAdNetwork 4.0 postback schema. The signature below is an illustrative placeholder and will not pass cryptographic verification; for unit testing, use Apple’s signed examples from the official verification documentation:

{
  "version": "4.0",
  "ad-network-id": "example123.skadnetwork",
  "source-identifier": "4821",
  "app-id": 1234567890,
  "transaction-id": "6a8b1c2d-3e4f-5a6b-7c8d-9e0f1a2b3c4d",
  "redownload": false,
  "source-app-id": 9876543210,
  "fidelity-type": 1,
  "did-win": true,
  "postback-sequence-index": 0,
  "conversion-value": 47,
  "attribution-signature": "MEQCIFz8...SAMPLE_CRYPTOGRAPHIC_SIGNATURE...=="
}

SKAN 4 signature message serialization with U 2063

Handling Multi-Window SKAN 4.0 Payloads and Developer Endpoints

Parsing postback-sequence-index Across Sequential Conversion Windows

In SKAdNetwork 4.0, conversions generate postbacks from conversion windows spanning up to 35 days after first app launch, with actual delivery occurring after Apple’s randomized post-window delays. Backend ingestion systems parse the postback-sequence-index field to assign conversion data to the correct lifecycle window:

  • Index 0 (Window 1: Day 0–2): Contains either a fine-grained conversion value (0–63) or a coarse-grained conversion value (low, medium, high), or the field is absent.
  • Index 1 (Window 2: Day 3–7): For postback data tiers 1–3, may disclose a coarse-conversion-value (low, medium, high) when provided; tier 0 is not eligible for second or third postbacks.
  • Index 2 (Window 3: Day 8–35): For postback data tiers 1–3, may disclose a coarse-conversion-value (low, medium, high) when provided; tier 0 is not eligible for second or third postbacks.

Managing Fine-Grained vs Coarse-Grained Conversion Values

Ingestion decoders must account for payload variability:

  • Mutual Exclusivity: Apple specifies that an install-validation postback may contain either conversion-value or coarse-conversion-value, but never both simultaneously.
  • Absent Conversion Values: If the assigned postback data tier is low (Tier 0), conversion value fields are omitted from the JSON payload.

Defending Against Replay Attacks and Spoofed Conversion Payloads

The Role of the transaction-id as a Deduplication Key

Every SKAdNetwork postback contains a unique transaction-id UUID. Apple documentation advises receivers to use this identifier as an idempotency key to detect and discard duplicate conversion postbacks.

Because postback listeners are publicly reachable HTTPS endpoints, malicious actors could attempt replay attacks by capturing a valid postback and resubmitting it repeatedly to artificially inflate conversion metrics.

Implementing Distributed In-Memory Caching and Persistent Ledgers

Apple does not prescribe a universal deduplication retention period. Production receivers should maintain a durable idempotency record for verified transaction IDs according to their reconciliation and replay-defense requirements; Redis TTL may be used as a hot-cache optimization rather than the sole authoritative duplicate ledger:

  1. Cryptographic Verification First: Fully verify the ECDSA signature against Apple’s public key before committing the transaction ID to storage.
  2. Atomic Deduplication: Perform an atomic write operation (e.g., Redis SET key value NX EX <seconds>) backed by a persistent relational or document database unique constraint.
  3. Deduplication Horizon: Set an operational retention window in the cache layer that covers expected postback delivery, network retries (Apple retries failed deliveries for up to 9 days), and downstream reconciliation.

SKAdNetwork replay defense and deduplication pipeline


The backend implementation below demonstrates signature validation, schema validation, and atomic deduplication in Python:

import base64
import json
import redis
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import load_der_public_key
from cryptography.exceptions import InvalidSignature

# Initialize Redis client for hot-cache transaction deduplication
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

# Official Apple SKAdNetwork 2.1+ Public Key (Base64 DER encoded, published by Apple)
APPLE_SKAN_PUBLIC_KEY_B64 = (
    "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWdp8GPcGqmhgzEFj9Z2nSpQVdday"
    "aPe4FMzqM9wib1+aHaaIzoHoLN9zW4K8y4SPykE3YVK3sVqW6Af0lfx3gg=="
)

# Exact Apple-specified invisible separator: U+2063 INVISIBLE SEPARATOR (UTF-8: 0xE2 0x81 0xA3)
SEPARATOR = "\u2063"

def construct_skan4_message_bytes(payload: dict) -> bytes:
    """
    Constructs the serialized UTF-8 message string for SKAN 4.0 signature verification.
    Apple specification explicitly EXCLUDES conversion-value and coarse-conversion-value from the signature.
    """
    parts = [
        str(payload["version"]),
        str(payload["ad-network-id"]),
        str(payload["source-identifier"]),
        str(payload["app-id"]),
        str(payload["transaction-id"]),
        "true" if payload["redownload"] is True else "false"
    ]

    # Include source-app-id (app ad) OR source-domain (web ad) if present
    if payload.get("source-app-id") is not None:
        parts.append(str(payload["source-app-id"]))
    elif payload.get("source-domain") is not None:
        parts.append(str(payload["source-domain"]))

    parts.append(str(payload["fidelity-type"]))
    parts.append("true" if payload["did-win"] is True else "false")
    parts.append(str(payload["postback-sequence-index"]))

    # Join with U+2063 separator and encode to UTF-8
    message_string = SEPARATOR.join(parts)
    return message_string.encode('utf-8')

def verify_and_ingest_skan4_postback(postback_json_str: str) -> dict:
    """
    Validates payload schema, verifies the ECDSA P-256 signature against Apple's public key,
    and performs atomic transaction-id deduplication.
    """
    try:
        payload = json.loads(postback_json_str)
    except Exception:
        return {"status": "REJECTED", "reason": "INVALID_JSON_FORMAT"}

    # Version Gate: Enforce SKAN 4.0 payload handling
    if payload.get("version") != "4.0":
        return {"status": "REJECTED", "reason": "UNSUPPORTED_SKAN_VERSION"}

    # Schema Validation: Required fields for SKAN 4.0
    required_fields = [
        "version", "ad-network-id", "source-identifier", "app-id",
        "transaction-id", "redownload", "fidelity-type", "did-win",
        "postback-sequence-index", "attribution-signature"
    ]
    for field in required_fields:
        if field not in payload:
            return {"status": "REJECTED", "reason": f"MISSING_REQUIRED_FIELD_{field.upper()}"}

    # Strict Type Validation
    if not isinstance(payload["redownload"], bool):
        return {"status": "REJECTED", "reason": "INVALID_TYPE_REDOWNLOAD"}
    if not isinstance(payload["did-win"], bool):
        return {"status": "REJECTED", "reason": "INVALID_TYPE_DID_WIN"}
    if payload["postback-sequence-index"] not in (0, 1, 2):
        return {"status": "REJECTED", "reason": "INVALID_SEQUENCE_INDEX"}
    if payload["fidelity-type"] not in (0, 1):
        return {"status": "REJECTED", "reason": "INVALID_FIDELITY_TYPE"}

    # Enforce mutual exclusivity between source-app-id and source-domain
    has_source_app = payload.get("source-app-id") is not None
    has_source_domain = payload.get("source-domain") is not None
    if has_source_app and has_source_domain:
        return {"status": "REJECTED", "reason": "CONFLICTING_SOURCE_FIELDS"}

    signature_b64 = payload["attribution-signature"]
    transaction_id = payload["transaction-id"]

    try:
        # Step 1: Reconstruct the exact UTF-8 serialized message
        message_bytes = construct_skan4_message_bytes(payload)
        signature_der = base64.b64decode(signature_b64, validate=True)

        # Step 2: Verify ECDSA P-256 / SHA-256 signature using Apple's published public key
        apple_public_key = load_der_public_key(base64.b64decode(APPLE_SKAN_PUBLIC_KEY_B64))
        apple_public_key.verify(
            signature_der,
            message_bytes,
            ec.ECDSA(hashes.SHA256())
        )
    except InvalidSignature:
        return {"status": "REJECTED", "reason": "INVALID_CRYPTOGRAPHIC_SIGNATURE"}
    except Exception as e:
        return {"status": "ERROR", "reason": f"VERIFICATION_FAILED: {str(e)}"}

    # Step 3: Atomic Deduplication via Redis (Hot cache layer)
    # Note: In production, pair this hot cache with a persistent unique database constraint.
    # 14-day TTL (1,209,600 seconds) serves as an illustrative receiver cache policy covering retries.
    is_new = redis_client.set(f"skan_tx:{transaction_id}", "1", nx=True, ex=1209600)
    if not is_new:
        return {"status": "DUPLICATE", "reason": "TRANSACTION_ALREADY_PROCESSED"}

    return {
        "status": "VERIFIED",
        "transaction_id": transaction_id,
        "sequence_index": payload["postback-sequence-index"],
        "did_win": payload["did-win"]
    }

Ingesting Postbacks into Real-Time Bidding Models and CPA Optimizers

Decoupling Edge Ingestion from Asynchronous Processing

High-volume DSPs process substantial postback volumes during peak campaign periods. Synchronous downstream processing can introduce latency bottlenecks.

Enterprise architectures implement an asynchronous pipeline:

  1. Edge Receiver: Accepts the incoming HTTP POST, verifies signature authenticity, executes atomic deduplication on the transaction-id, and immediately returns HTTP 200 OK.
  2. Event Queue: Publishes the verified payload to a distributed event broker (e.g., Apache Kafka or AWS SQS).
  3. Bidding & Analytics Workers: Consumes the event stream, maps conversion values to revenue metrics, and updates Real-Time Bidding (RTB) target CPA models.

Utilizing the Hierarchical Source Identifier

The first winning postback may expose two, three, or four digits of the hierarchical source-identifier, depending on the postback data tier. The semantic meaning of those digits is defined by the ad network’s own source-identifier taxonomy. Bidding systems should resolve the received source-identifier against the network’s own campaign metadata rather than assume a universal mapping between digit length and specific placement or creative granularity.

Comparative Matrix: Direct Apple Delivery versus MMP S2S Ingestion

Functional Dimension Direct Apple Delivery (Ad Network) Developer Endpoint (NSAdvertising...) MMP S2S Ingestion Pipeline
Recipient Registered Ad Network Advertised App Developer Mobile Measurement Partner
Attribution Scope Winning Postbacks for that Network Copy of Winning Postbacks for the App Multi-Network Aggregated View
Signature Validation Executed by Ad Network Backend Executed by Developer Backend Implementation-dependent (Partner flows)
Nonwinning Postbacks Received if Qualified (did-win: false) Not Delivered to Developer Endpoint May be available through partner flows
Primary Use Case Direct Bidder & Target CPA Optimization Internal Warehouse Auditing & Verification Cross-Channel Performance Dashboard

Frequently Asked Questions (FAQ)

What public key is used to verify Apple's postback signature?
Apple publishes the official NIST P-256 public key used for SKAdNetwork 2.1+ install validation in its developer documentation. Ingestion servers load this public key in X.509/DER format to verify incoming signatures.
Why does a valid SKAdNetwork postback fail signature verification?
Signature verification failures typically occur due to serialization errors: using the wrong separator character (using `\u2060` instead of `\u2063`), incorrect parameter ordering, erroneously appending conversion values to SKAN 4 message strings, or incorrectly handling boolean string encodings (`"true"` vs `"false"`).
Can the advertised app's developer endpoint receive nonwinning SKAdNetwork postbacks?
No. The developer copy endpoint receives copies of winning install-validation postbacks when configured. Up to five nonwinning postbacks (`did-win: false`) are sent directly to other qualifying ad networks, not to the developer copy endpoint.

Summary and Decision Framework

Handling SKAdNetwork postbacks at scale requires combining low-latency edge ingestion with rigorous cryptographic validation and transaction-level deduplication. Because Apple postbacks directly influence budget allocation and bidding algorithms, validating ECDSA signatures and enforcing transaction-id idempotency protects ingestion pipelines from forged or tampered postbacks and duplicate replay processing.

To complement platform-mediated SKAdNetwork reporting with micro-level user onboarding and instant deep link routing, engineering teams deploy first-party routing architectures alongside platform APIs.

To learn more about configuring server-side attribution postbacks and deep linking pipelines, review the OpoInstall documentation.

Related Materials

  • Concepts: S2S Postbacks, Cryptographic Verification, ECDSA P-256, Replay Attack Defense, Transaction Deduplication

  • Technologies: Apple SKAdNetwork, Apple AdAttributionKit, Redis In-Memory Cache, OpoInstall Mobile SDK

  • Standards: IETF RFC 8259 (JSON Data Interchange), RFC 5480 (Elliptic Curve Cryptography)

  • APIs: StoreKit SKAdNetwork API, Apple S2S Postback Delivery Specification, OpoInstall S2S API

Official Documentation

Share this article