# IP Allowlisting

It is a security mechanism that restricts access.

As described in Authentication, we do not use static API keys or access tokens: every request is signed with the private key of a Project or Organization credential, and the private key is never transmitted.

An IP allowlist adds a second, independent condition on top of that signature: the addresses your credential's requests are allowed to come from. A request is accepted only if the signature is valid and it arrives from one of those addresses.

This is about traffic from your integration to us — the opposite direction of our Static IP list, which holds the addresses we call your webhook endpoints from and that you allow in your own firewall.

The allowlist belongs to the credential, not to the public key file. Rotating the public key of a credential preserves its allowlist, and a new credential always starts without one.

**REQUEST ACCEPTANCE RULE**

accept = valid_ecdsa_signature(credential) AND source_ip IN allowed_ips

## How It Works

A credential's IP allowlist is always in one of three states:

| State | Behavior | How it is reached |
| --- | --- | --- |
| notConfigured | Requests are accepted from any IP, as long as authentication is valid | Default for every new credential |
| active | Only requests coming from the registered addresses are accepted | You enable the allowlist and save between 1 and 10 addresses |
| disabled | Requests are accepted from any IP again, and the change is recorded | You disable the allowlist explicitly |

While the allowlist is not active, requests with a valid signature are accepted from any IP. When you save and enable it, the restriction applies immediately — no scheduling, no grace period.

Sandbox and Production keep independent allowlists. There is no automatic synchronization between them, so enabling it in Sandbox lets you validate the whole setup before touching your live integration.

## Errors

A request that is correctly signed but comes from an address outside an active allowlist is refused with 403 and the error code invalidIp, before it reaches the resource. The message reports the address we observed, and the response carries a request identifier you can send us. See Errors for the general error format.

The allowlist is only evaluated after authentication succeeds — a request with an invalid signature is rejected as an authentication failure and never reveals whether its address would have been allowed.

An active allowlist must always contain at least one address, so removing the last one is refused with 400 and the code emptyIpAllowlist. Disabling the allowlist is a separate, explicit action.

If the allowlist's state can't be determined, the request fails as a temporary error rather than being accepted.

**REQUEST FROM AN ADDRESS THAT IS NOT ALLOWED**

HTTP/1.1 403 Forbidden
Request-Id: req_01JABCDEF

{
    "errors": [
        {
            "code": "invalidIp",
            "message": "The request origin IP 198.51.100.24 is not allowed for this credential."
        }
    ]
}

**REMOVING THE LAST ADDRESS OF AN ACTIVE ALLOWLIST**

HTTP/1.1 400 Bad Request

{
    "errors": [
        {
            "code": "emptyIpAllowlist",
            "message": "An enabled IP allowlist must contain at least one address. Add another address or disable the allowlist before removing this one."
        }
    ]
}

## Set Up

You manage the allowlist in our Web Banking, on the screen where the credential and its public key are managed. The allowlist belongs to the credential, so you can add or remove addresses at any time without generating a new key pair.

1. Open the credential in Sandbox or Production.

2. Enable IP allowlisting.

3. Add between 1 and 10 IPv4 addresses.

4. Confirm and save — the change takes effect immediately.

Use your server's outbound address — its NAT gateway or proxy — not your browser's. Registering the browser address is the most common setup mistake.

| Rule | Value |
| --- | --- |
| Addresses per credential | Between 1 and 10 while it is active |
| Accepted format | Individual public IPv4 addresses |
| CIDR ranges | Not accepted |
| IPv6 | Not supported in this version |
| Duplicates | Rejected |
| Private, loopback, link-local and multicast addresses | Rejected |
| Updates | Atomic: the complete set of addresses is replaced at once |

Register multiple addresses when you have more than one region, a failover path, or separate workloads. Add the new address before cutting over traffic, and remove the old one only after confirming the new path works.

Before you enable it:

Keep using an HSM or secret manager for your private keys — an IP allowlist is an extra layer, not a reason to relax key handling.
Use distinct credentials per environment and per workload, so one compromise has a bounded blast radius.
Map every system that calls our API, including batch jobs and third-party integrations, and register each one's real outbound address.
Enable it in Sandbox first, confirm your integration still works, then repeat in Production.

## Get a Static Outbound IP

Requests must leave your infrastructure through a fixed address you control. Check the real outbound address from inside the workload — not your load balancer or laptop — and repeat the check on every instance, task or function that calls our API.

Place the workload in a private subnet and route its outbound traffic through a managed NAT component that owns one reserved address. That address is the one you register. The workload itself must have no public IP of its own, or it bypasses the NAT and egresses through that address instead.

| Provider | Component | Static address type |
| --- | --- | --- |
| AWS | NAT Gateway | Elastic IP that you allocate |
| Azure | NAT Gateway, or Azure Firewall | Static public IP that you allocate |
| Google Cloud | Cloud NAT (Cloud Router) | External IP that you reserve |
| Oracle Cloud (OCI) | NAT Gateway | Reserved public IP that you create |

On AWS, use replace-route instead of create-route if the route table already has a default route — for example when replacing an existing NAT Gateway. On Oracle Cloud, read the route table's existing rules before updating it, since the update replaces the whole rule set.

Serverless and managed container runtimes egress through provider-owned pools by default and aren't stable. Attach them to your own VPC/VNet where possible, or use a dedicated proxy.

**CHECK YOUR OUTBOUND ADDRESS**

curl --fail --silent https://ifconfig.me

**AWS — NAT GATEWAY**

aws ec2 allocate-address --domain vpc

aws ec2 create-nat-gateway --subnet-id PUBLIC_SUBNET_ID \
    --allocation-id ALLOCATION_ID

aws ec2 create-route --route-table-id PRIVATE_ROUTE_TABLE_ID \
    --destination-cidr-block 0.0.0.0/0 --nat-gateway-id NAT_GATEWAY_ID

aws ec2 describe-addresses --allocation-ids ALLOCATION_ID \
    --query 'Addresses[0].PublicIp' --output text
curl --fail --silent https://ifconfig.me

**AZURE — NAT GATEWAY**

az network public-ip create --resource-group RESOURCE_GROUP \
    --name starkbank-egress --location LOCATION \
    --sku Standard --allocation-method Static

az network nat gateway create --resource-group RESOURCE_GROUP \
    --name starkbank-nat --location LOCATION \
    --public-ip-addresses starkbank-egress

az network vnet subnet update --resource-group RESOURCE_GROUP \
    --vnet-name VNET --name SUBNET --nat-gateway starkbank-nat

az network public-ip show --resource-group RESOURCE_GROUP \
    --name starkbank-egress --query ipAddress --output tsv
curl --fail --silent https://ifconfig.me

**GOOGLE CLOUD — CLOUD NAT**

gcloud compute addresses create starkbank-egress --region=REGION

gcloud compute routers create starkbank-router \
    --network=VPC_NETWORK --region=REGION

gcloud compute routers nats create starkbank-nat \
    --router=starkbank-router --region=REGION \
    --nat-custom-subnet-ip-ranges=SUBNET \
    --nat-external-ip-pool=starkbank-egress

gcloud compute addresses describe starkbank-egress \
    --region=REGION --format='value(address)'
curl --fail --silent https://ifconfig.me

**ORACLE CLOUD — NAT GATEWAY**

oci network public-ip create --compartment-id COMPARTMENT_OCID \
    --lifetime RESERVED --display-name starkbank-egress

oci network nat-gateway create --compartment-id COMPARTMENT_OCID \
    --vcn-id VCN_OCID --display-name starkbank-nat \
    --public-ip-id RESERVED_PUBLIC_IP_OCID

oci network route-table get --rt-id ROUTE_TABLE_OCID \
    --query 'data."route-rules"'

oci network route-table update --rt-id ROUTE_TABLE_OCID \
    --route-rules '[EXISTING_RULES, {"cidrBlock":"0.0.0.0/0",
                    "networkEntityId":"NAT_GATEWAY_OCID"}]'

oci network nat-gateway get --nat-gateway-id NAT_GATEWAY_OCID \
    --query 'data."nat-ip"'
curl --fail --silent https://ifconfig.me

## No Fixed Egress Available

If your platform can't offer a stable address, put a small dedicated proxy — one reserved public address, in a network you control — between your workload and our API. Register the proxy's address.

Keep signing in your application — the proxy forwards traffic, it must never hold your private key or re-sign requests.
Don't terminate TLS unless required — forwarding the encrypted connection keeps the signature intact.
Restrict who can reach the proxy to your own workloads.
Run at least two instances, each registered, so the proxy isn't a single point of failure.

**TRAFFIC PATH**

your workload (any/rotating IP)
    |
    v
egress proxy (fixed public IP — the address you register)
    |
    v
api.starkbank.com
