Docs/Mobile SDK/Concepts

Gateway routing

In short

TransportPolicy (OctetConfig.advanced.transport, new in 3.0) chooses where the SDK's network calls go: straight to Octet, through an HTTPS host you run, or through Octet's hosted gateway.

By default the SDK talks to the Octet backend at api.octetproof.com and, for a few enrichment calls, to a third-party host: IP location on iOS, and GNSS broadcast-ephemeris servers on Android. If your app's network surface is audited, for example by a firewall allowlist, an app-store network disclosure or a rule that the app talks only to your own domain, you can route every SDK request through one host.

Content uploads are the exception. Uploads go straight to the URL your provider returns. A gateway and its certificate pins don't apply, so allow your storage host if you restrict egress. See Uploading content.

The three modes

Mode Swift Kotlin Where calls go
Direct (default) .direct Mode.DIRECT Octet and the third-party hosts, as in 2.0.
Integrator gateway .integratorGateway Mode.INTEGRATOR_GATEWAY Every first-party call, and the third-party lookups, go to the HTTPS host you set in gateway.
Octet gateway .octetGateway Mode.OCTET_GATEWAY Every call goes to Octet's hosted gateway.

In both gateway modes, the SDK sends every request to {gateway}{path}. Activation, verification, and proof and flag signing still terminate at Octet. The gateway only forwards requests, so the trust model is unchanged and only the network path moves.

// Your own gateway
let config = OctetConfig(
    advanced: AdvancedConfig(
        transport: TransportPolicy(
            mode: .integratorGateway,
            gateway: "https://api.example.com/octet")))

// Octet's gateway
let hosted = OctetConfig(
    advanced: AdvancedConfig(transport: .octetGateway))
// Your own gateway
val config = OctetConfig(
    advanced = AdvancedConfig(
        transport = TransportPolicy(
            mode = TransportPolicy.Mode.INTEGRATOR_GATEWAY,
            gateway = "https://api.example.com/octet")))

// Octet's gateway
val hosted = OctetConfig(
    advanced = AdvancedConfig(transport = TransportPolicy.octetGateway))

Octet gateway

The Octet gateway gives the app a single backend domain without a proxy for you to run. The SDK sends every call, first-party and third-party, to Octet's hosted gateway. The host is built in, so there is no gateway to supply and any value you set is ignored. This mode uses standard certificate-authority validation, and pins is ignored. fallbackToDirect applies as in the integrator-gateway mode. On iOS, the Octet gateway also serves the IP-based country hint.

TransportPolicy options

Option Default Effect
mode direct One of the three modes above.
gateway nil / null Base URL of your gateway. Used only in the integrator-gateway mode.
thirdParty .proxy / ThirdParty.PROXY Sends third-party lookups through the gateway. .disable / ThirdParty.DISABLE skips them in any mode, and a proof is still produced.
pins empty SPKI pins for the app-to-gateway TLS connection. Ignored in the Octet-gateway mode.
fallbackToDirect false After repeated gateway failures, critical calls go direct until the gateway recovers.
fallbackAfterFailures 3 How many transport failures in a row trip the fallback.
osAttestationInGatewayModes true false skips per-proof OS attestation in gateway modes, so proofs carry no hardware attestation.

What your gateway must forward

This table covers the integrator-gateway mode.

The SDK sends (under your gateway) Forward it to Notes
/v1/bootstrap/challenge, /v1/bootstrap, /v1/heartbeat https://api.octetproof.com/v1/… Activation. Never modify the body.
/v1/flags, /v1/metrics, /v1/credits/… https://api.octetproof.com/v1/… Flag bundles are signed. Never modify.
/v1/proofs, /v1/proofs/auth, /v1/proofs/challenge https://api.octetproof.com/v1/… Proof body up to 256 KiB. Never modify.
/v1/exposure https://api.octetproof.com/v1/… Both SDKs. Usage reporting, sent only with telemetryEnabled on.
/v1/network/observe https://api.octetproof.com/v1/… Both SDKs. A GET with no body.
/v1/integrity/decode https://api.octetproof.com/v1/… Both SDKs. Sent only when your app calls Octet.verifyOnline with a Play Integrity decoder.
/ext/ipgeo/… Optional, see IP-based country hint iOS SDK only. IP location hint.
/ext/gnss/… https://cddis.nasa.gov/archive/gnss/data/daily/… (fallback https://igs.ign.fr/pub/igs/data/…) Android SDK only. GNSS ephemeris. Large files, and the upstream may need NASA Earthdata credentials.
/ext/geo/… https://api.octetproof.com/ext/geo/… Both SDKs. First-party and bearer-authenticated, so forward it like a /v1/… route: keep Authorization and send Host: api.octetproof.com.

Forward /ext/geo/ on both platforms: without it, country and subdivision proofs return INDETERMINATE (REGION_UNRESOLVED). fallbackToDirect does not cover a missing route, because it trips only on transport failures. Forward /ext/gnss/ only if you ship the Android SDK.

Forwarding rules

  • Preserve the request exactly. Keep the method, the path after your prefix, the query string, and all headers, especially Authorization, Content-Type, and any X-Octet-… header.
  • Never modify the body of /v1/proofs, /v1/flags, /v1/bootstrap/challenge or /v1/bootstrap. Each signature covers the exact bytes, so any rewrite, including re-serializing the JSON, breaks verification.
  • Use TLS 1.2 or higher to the upstream, verify the upstream certificate, and send Host: api.octetproof.com with matching SNI on the first-party routes.
  • Pass status codes and the Retry-After header through unchanged. The SDK honors 429.
  • Allow request bodies of at least 256 KiB, and give the upstream a timeout at least as long as the SDK's. Activation and attestation can be slow.

Reference configurations

Replace api.example.com/octet with your own gateway base URL, and keep only the ext blocks for the platforms you ship.

nginx

# All first-party routes are under /v1/: activation, flags, metrics, credits,
# proofs (incl. /v1/proofs/auth + /v1/proofs/challenge).
location /octet/v1/ {
    proxy_pass                  https://api.octetproof.com/v1/;
    proxy_ssl_server_name       on;
    proxy_set_header Host       api.octetproof.com;
    proxy_pass_request_headers  on;
    client_max_body_size        256k;
}
# Android apps only:
location /octet/ext/gnss/  { proxy_pass https://cddis.nasa.gov/archive/gnss/data/daily/; proxy_ssl_server_name on; }
# All apps (first-party, bearer-authed):
location /octet/ext/geo/   { proxy_pass https://api.octetproof.com/ext/geo/; proxy_ssl_server_name on; proxy_set_header Host api.octetproof.com; }

Cloudflare Worker

const UPSTREAM = {
  "/v1/":        "https://api.octetproof.com/v1/",                   // all first-party (incl. /v1/proofs/auth, /v1/proofs/challenge)
  "/ext/gnss/":  "https://cddis.nasa.gov/archive/gnss/data/daily/",  // Android apps
  "/ext/geo/":   "https://api.octetproof.com/ext/geo/",              // all apps (first-party)
};

export default {
  async fetch(request) {
    const url = new URL(request.url);
    const path = url.pathname.replace(/^\/octet/, "");   // strip your gateway prefix
    const match = Object.entries(UPSTREAM).find(([prefix]) => path.startsWith(prefix));
    if (!match) return new Response("not found", { status: 404 });
    const [prefix, base] = match;
    const target = base + path.slice(prefix.length) + url.search;
    // Forward method, headers, and body untouched; return the upstream response as-is.
    return fetch(target, {
      method: request.method,
      headers: request.headers,
      body: request.body,
      redirect: "follow",
    });
  },
};

AWS

  • API Gateway (HTTP API). Add one HTTP-proxy integration per prefix. For example, route ANY /octet/v1/{proxy+} to https://api.octetproof.com/v1/{proxy}, and add one each for /octet/ext/geo/ and, for Android, /octet/ext/gnss/. Leave request-parameter mapping untouched so headers and the query string pass through unchanged.
  • CloudFront. Define one origin per upstream and one cache behavior per path pattern (/octet/v1/*, /octet/ext/geo/*, /octet/ext/gnss/*). Attach an origin-request policy that forwards all headers, query strings, and cookies, and the managed CachingDisabled cache policy so nothing is buffered or rewritten. Set each origin to HTTPS-only.

Gateway-mode settings

Certificate pinning

The SDK's built-in pins are for Octet's certificates, so they do not apply once traffic goes through your gateway: the app sees your certificate. To keep pinning on the app-to-gateway connection, pass your gateway's SPKI pins in pins:

transport: TransportPolicy(
    mode: .integratorGateway,
    gateway: "https://api.example.com/octet",
    pins: ["<base64-sha256-of-your-proxy-SPKI>", "<backup-pin>"])
transport = TransportPolicy(
    mode = TransportPolicy.Mode.INTEGRATOR_GATEWAY,
    gateway = "https://api.example.com/octet",
    pins = listOf("<base64-sha256-of-your-proxy-SPKI>", "<backup-pin>"))

Compute a pin with:

openssl x509 -in cert.pem -pubkey -noout | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 -binary | openssl enc -base64

Pin a stable point in the chain, such as a CA intermediate or root. A managed certificate (Cloudflare, ACME, Let's Encrypt) gets a new key on renewal, which breaks a pin on the leaf certificate the next time it rotates. Always include a backup pin. Leave pins empty, the default, to rely on standard certificate-authority validation.

Fallback to direct

fallbackToDirect is off by default, so a gateway outage makes the SDK's calls fail. Set it to true to keep the SDK working through an outage. After fallbackAfterFailures transport-level failures in a row (connect, TLS, DNS or timeout, and never an HTTP status), the SDK sends the critical calls (activation, proof upload, and the daily usage record) directly to Octet. It then checks your gateway again and switches back once the gateway recovers. While the fallback is active, those calls go to api.octetproof.com directly, so the app no longer talks to a single domain. A deployment that must keep to one domain should leave it off. Telemetry and flags always stay on the gateway.

transport: TransportPolicy(
    mode: .integratorGateway,
    gateway: "https://api.example.com/octet",
    fallbackToDirect: true,
    fallbackAfterFailures: 3)
transport = TransportPolicy(
    mode = TransportPolicy.Mode.INTEGRATOR_GATEWAY,
    gateway = "https://api.example.com/octet",
    fallbackToDirect = true,
    fallbackAfterFailures = 3)

OS attestation in gateway modes

In a gateway mode, the SDK still attaches OS attestation to proofs by default, at the configured attestationCadence. That includes App Attest on iOS, which calls Apple, and Play Integrity on Android, which calls Google. These calls cannot go through a proxy. Set osAttestationInGatewayModes to false to skip them. The proofs then carry no App Attest evidence on iOS, and no key-attestation chain and no Play Integrity verdict on Android. They are still signed by the device key and verify as authentic, but they are not hardware-attested. octet-verify reports them as attested: false in --json output and fails them under --require-attestation. Octet.verify reports hardware-attestation as NOT-CHECKED and fails them when requireAttestation is set. Gate acceptance on attestation, not on validity alone. Startup activation still attests, so the app still activates.

transport: TransportPolicy(
    mode: .octetGateway,
    osAttestationInGatewayModes: false)   // un-attested proofs
transport = TransportPolicy(
    mode = TransportPolicy.Mode.OCTET_GATEWAY,
    osAttestationInGatewayModes = false)   // un-attested proofs

IP-based country hint

This applies to the iOS SDK. With your own gateway, Octet provides no upstream for /ext/ipgeo/. Leave that route unrouted, or set TransportPolicy.thirdParty to .disable so the SDK skips the lookup. The hint is optional, and a proof is still produced either way. The Octet gateway serves the hint itself.

Where to go next

TermsPrivacy© 2026 Understone, Inc. d/b/a Octet