Peering Portal API
Peering Manager exposes a dedicated API surface at /api/peering/portal/
designed to be consumed by an external "peering portal": a public-facing
website that lets other networks request BGP peering with the AS operating
Peering Manager.
The portal API is limited to portal-specific operations: looking up a requesting network, discovering common peering locations, submitting a peering request, and tracking the status of past requests. Operator decisions (accept and reject) and listing of established BGP sessions are served by the standard REST API.
This page describes how a custom portal interacts with Peering Manager. The design follows the IETF draft draft-ramseyer-grow-peering-api where compatible.
You may not have to build one
Peering Portal is a
ready to run portal that speaks this API, published as
peeringmanager/peering-portal and
ghcr.io/peering-manager/peering-portal. It walks a network through a
form based wizard, signs the visitor in with PeeringDB, and needs only
the URL and the API token of a Peering Manager instance. Read on if you
would rather write your own.
Glossary
| Term | Meaning |
|---|---|
| Affiliated AS | The autonomous system the Peering Manager operator runs and accepts peering for. Selected per-user in the top-right menu ("Select affiliated AS"). |
| Peer AS / Local AS | The external network asking to peer. In submission payloads this is local_asn (it is local from the requesting portal user's perspective). |
| Requesting ASN | Same as Peer AS, used in URL and query parameters as asn. |
| Peering request | A submission from an external network, identified by a UUID request_id. |
| Requested session | One BGP session entry inside a peering request, identified by session_id. |
| Tracking ID | The UUID (request_id) that the portal hands back to the requester so they can poll status or cancel without authentication. |
| Local IP / Peer IP | Inside a session payload, local_ip is the requesting network's IP and peer_ip is the operator's IP on the connection. The pair (peer_ip, location) uniquely identifies which operator connection a session targets. |
| Location | Either an IXLan (public peering, format pdb:ix:<ixlan_id>) or a PeeringDB facility (private peering, format pdb:fac:<facility_id>). |
Architecture Overview
A typical deployment looks like this:
+-----------------+ +-------------------+ +-----------------+
| End user | <-----> | Peering portal | <----> | Peering Manager |
| (web browser) | HTML | (your custom app) | REST | (this app) |
+-----------------+ +-------------------+ +-----------------+
|
| reads PeeringDB cache,
| writes peering requests,
| reads existing sessions
The portal sits between the end user and Peering Manager:
- The end user never talks to Peering Manager directly.
- The portal holds the Peering Manager API token and forwards requests on behalf of users.
- The operator reviews requests through Peering Manager's standard UI or REST API.
The portal authenticates to Peering Manager with a single API token, so do not embed that token in client-side code. The portal must proxy requests server-side and keep the token secret.
Authentication and Permissions
All portal API endpoints require a Peering Manager API token authenticated
via the Authorization: Token <key> HTTP header.
The user account that owns the token needs:
- Both the Add peering request and Change peering request permissions.
- An affiliated AS selected from the top-right user menu. The portal API uses this AS as the peer side (the AS that the external network is asking to peer with) and scopes every query to it. Requests targeting any other affiliated AS are not visible to this token.
If a portal endpoint is called by a token whose owner has no affiliated AS set, it returns a HTTP 422 with an explicit error message.
The portal can ask Peering Manager which AS the token is bound to:
curl -s -H "Authorization: Token $PM_API_TOKEN" \
"$PM_API_URL/peering/portal/affiliated"
{"asn": 64496, "name": "Example"}
This is useful for showing the user "you are peering with AS64496" without hard-coding it on the portal side.
ASN ownership is the portal's responsibility
Peering Manager authenticates the portal as a whole via a single API token. It has no notion of which end user is making a particular call and does not verify that the ASN supplied in a request actually belongs to the requesting user. Every request that reaches Peering Manager looks like it came from the portal itself.
The portal must verify that a user is authorised to act on behalf of the ASN they claim before forwarding any submission, cancellation, or status lookup. Without this check, any visitor could file or cancel peering requests on behalf of an arbitrary network.
The recommended way is to delegate authentication to PeeringDB:
- Use PeeringDB OAuth to log the user in. The OAuth flow returns the PeeringDB user account, which lists the networks (ASNs) that user is affiliated with on PeeringDB.
- Restrict every request operation in the portal to ASNs that appear in the authenticated user's affiliations.
Peering Portal does exactly this, so running it saves you from writing the check.
Example Portal Workflow
Below is a walkthrough for a simple portal that lets a user submit a peering
request (over an IXP or in a private facility), look up past requests, and
view existing peering sessions. All examples use curl for clarity, but any
HTTP client will work.
Step 1, look up the requesting network
The portal asks the user to enter the ASN of their network. That ASN must have a record in the local PeeringDB cache, otherwise no information can be retrieved and no common locations can be computed.
curl -s -H "Authorization: Token $PM_API_TOKEN" \
"$PM_API_URL/peering/portal/network/64500"
{
"asn": 64500,
"name": "Example Network",
"name_long": "Example Network Inc.",
"info_prefixes4": 100,
"info_prefixes6": 50,
"irr_as_set": "AS-EXAMPLE",
"policy_general": "Open",
"contacts": [
{"name": "NOC", "email": "noc@example.net", "role": "Technical"}
]
}
The portal can use this response to pre-fill the request form (network name, maximum prefix counts, contact email).
A HTTP 404 response means the ASN is not present in the PeeringDB cache.
Step 2, discover common peering locations
Once the ASN is validated, the portal asks Peering Manager which IXPs and facilities are shared between the requester and the affiliated AS:
# All locations (IXPs + facilities)
curl -s -H "Authorization: Token $PM_API_TOKEN" \
"$PM_API_URL/peering/portal/locations?asn=64500"
# IXPs only
curl -s -H "Authorization: Token $PM_API_TOKEN" \
"$PM_API_URL/peering/portal/locations?asn=64500&location_type=public"
# Facilities only (for private peering)
curl -s -H "Authorization: Token $PM_API_TOKEN" \
"$PM_API_URL/peering/portal/locations?asn=64500&location_type=private"
{
"peer_asn": 64496,
"locations": [
{
"location": "pdb:ix:42",
"name": "Example IX",
"peering_type": "public",
"sessions": [
{
"local_ip": "192.0.2.1/24",
"peer_ip": "192.0.2.254/24",
"address_family": 4,
"existing": false
},
{
"local_ip": "192.0.2.1/24",
"peer_ip": "192.0.2.253/24",
"address_family": 4,
"existing": false
},
{
"local_ip": "2001:db8::1/64",
"peer_ip": "2001:db8::ffff/64",
"address_family": 6,
"existing": false
}
]
},
{
"location": "pdb:fac:17",
"name": "Example Facility",
"peering_type": "private",
"sessions": []
}
]
}
For each public location, sessions lists possible BGP sessions based on
the requester's PeeringDB presence. The existing flag means a session
with that IP is already configured in Peering Manager.
A BGP session is uniquely identified by the pair (peer IP, operator
connection). One peer IP can pair with several operator connections on the
same IX (multi-router setups), and a peer can also have several IPs on the
same IXLan, so sessions is the cartesian product of those two axes per
address family: one row per pair. Rows are emitted only when at least one
operator connection exists for the address family.
local_ip values come from PeeringDB host records, returned with the
matching IXLan prefix length appended (for example 192.0.2.1/24) so they
are directly usable as BGP session IP fields. peer_ip is the operator IP
for that specific pair.
When the portal submits, each session entry includes local_ip, peer_ip,
and location. That triple unambiguously identifies one operator
connection.
For private locations, the user enters both IP addresses themselves (the
PeeringDB cache does not know which IPs are negotiated on a private
interconnect): local_ip for their own end, peer_ip for the operator's
end. Both are required at submission time, because the operator-side IP
on a private link cannot be guessed from anything else except trivial
/31 or /127 cases. The location value for a facility uses its PeeringDB
ID (for example "pdb:fac:17").
Step 3, submit the peering request
Once the user picks the locations and (optionally) provides session parameters, the portal submits the request:
# Public peering (IXP) example
curl -s -H "Authorization: Token $PM_API_TOKEN" \
-H "Content-Type: application/json" \
-X POST "$PM_API_URL/peering/portal/sessions" \
-d '{
"local_asn": 64500,
"peer_type": "public",
"email": "noc@example.net",
"sessions": [
{"local_ip": "192.0.2.1/24", "peer_ip": "192.0.2.254",
"location": "pdb:ix:42"},
{"local_ip": "2001:db8::1/64", "peer_ip": "2001:db8::ffff",
"location": "pdb:ix:42", "session_secret": "s3cret"}
]
}'
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"sessions_count": 2
}
peer_ip is matched on host address only, so both bare IPs
(192.0.2.254) and IPs with prefix length (192.0.2.254/24) are accepted.
A private peering request looks the same except peer_type is "private"
and location carries the facility ID (as returned in the locations
field of the location lookup):
curl -s -H "Authorization: Token $PM_API_TOKEN" \
-H "Content-Type: application/json" \
-X POST "$PM_API_URL/peering/portal/sessions" \
-d '{
"local_asn": 64500,
"peer_type": "private",
"email": "noc@example.net",
"sessions": [
{
"local_ip": "192.0.2.10/30",
"peer_ip": "192.0.2.9/30",
"location": "pdb:fac:17"
}
]
}'
The response contains a request_id, a UUID acting as the public-facing
tracking identifier.
Note
Submitting a request does not create any BGP sessions or
AutonomousSystem records in Peering Manager. Those are only created
if the operator accepts the request (see
Operator decisions below).
The submission is rejected with a HTTP 409 in two cases, distinguished by
the machine-readable code field; conflicting_ips always lists the IPs
at fault:
-
Pending duplicate (
code:duplicate_pending): the requester already has a pending peering request that contains one of the submitted IPs:{ "detail": "Duplicate request: sessions with these IPs are already pending.", "code": "duplicate_pending", "conflicting_ips": ["192.0.2.1"] } -
Existing session conflict (
code:already_configured): one of the submitted sessions matches a BGP session already configured in Peering Manager (same(operator connection, IP)for public peering, or same(peer ASN, IP)for private peering):{ "detail": "Sessions with these IPs are already configured.", "code": "already_configured", "conflicting_ips": ["192.0.2.1"] }
Step 4, show the user their past requests
Because the portal is stateless on the server side, past requests are
discovered via the tracking IDs. Requests can also be listed for an ASN
with GET /api/peering/portal/sessions?asn={asn}, with an optional
request_id filter. For each known tracking ID, the portal queries:
curl -s -H "Authorization: Token $PM_API_TOKEN" \
"$PM_API_URL/peering/portal/sessions/550e8400-e29b-41d4-a716-446655440000"
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "accepted",
"peer_type": "public",
"local_asn": 64500,
"peer_asn": 64496,
"decision_comment": "",
"sessions": [
{
"session_id": 12,
"local_ip": "192.0.2.1/24",
"peer_ip": "192.0.2.254/24",
"location": "pdb:ix:42",
"location_name": "Example IX",
"status": "accepted",
"rejection_comment": ""
},
{
"session_id": 13,
"local_ip": "2001:db8::1/64",
"peer_ip": "2001:db8::ffff/64",
"location": "pdb:ix:42",
"location_name": "Example IX",
"status": "rejected",
"rejection_comment": "Auto-rejected: A session with IP 2001:db8::1/64 already exists on Example IX."
}
],
"created": "2026-04-22T08:30:00Z",
"updated": "2026-04-22T10:15:00Z"
}
A HTTP 404 response is intentionally generic so that an attacker guessing tracking IDs cannot enumerate other users' requests.
Step 5, cancel a pending request
While a request is still pending, the user can cancel it:
curl -s -H "Authorization: Token $PM_API_TOKEN" \
-X DELETE \
"$PM_API_URL/peering/portal/sessions/550e8400-e29b-41d4-a716-446655440000"
A HTTP 204 response means the request and all its pending sessions were marked as cancelled. A HTTP 409 response means the request had already been accepted or refused, so there is nothing to cancel. A HTTP 404 is returned for an unknown tracking ID (same anti-enumeration behaviour as the status endpoint).
Step 6, list existing peering sessions
The portal API itself does not expose established BGP sessions because they are operator-managed objects. To list them, use the standard REST API:
# All IXP sessions for a given ASN
curl -s -H "Authorization: Token $PM_API_TOKEN" \
"$PM_API_URL/peering/internet-exchange-peering-sessions/?autonomous_system_asn=64500"
# All direct (private) sessions for a given ASN
curl -s -H "Authorization: Token $PM_API_TOKEN" \
"$PM_API_URL/peering/direct-peering-sessions/?autonomous_system_asn=64500"
Each session response includes its current status (requested,
provisioning, enabled, disabled, etc.) and operational bgp_state
(idle, established, etc.) when polled. The portal can present these
alongside the user's own requests so the user has a single view of what is
already configured and what they are asking for.
See the REST API Overview for the full session schema.
Operator Decisions
Once a request is submitted, an operator reviews it through Peering Manager's web UI (under "Peering Requests") or via the REST API.
These actions are not part of the portal API surface. They are meant for the operator and require the Review peering requests permission, which is distinct from the Add / Change permissions the portal token holds.
What happens on accept
When the operator accepts a request:
- The requesting
AutonomousSystemrecord is automatically created from PeeringDB data (name, IRR AS-SET, max prefix counts) if it does not already exist. If the requesting ASN has no PeeringDB record, the affected sessions are auto-rejected with a comment. - For private peering requests, the operator must set a
relationshipon the request before accepting it. Otherwise the accept call returns an error. - For each accepted session, a BGP session is created with the initial
status defined by the
PEERING_REQUEST_SESSION_STATUSsetting. - If the requester supplied a
session_secret, it becomes the BGP session password. - For accepted private peering sessions, the
peer_ipsupplied by the requester is set as the operator-side local IP on the created direct session, so the operator does not have to fill it in by hand. - Sessions that can no longer be created (for example, because the operator
has manually configured a conflicting session in the meantime, or no IXP
connection with a matching address family is configured) are
automatically marked as
rejectedwith an explanatory comment.
Avoiding out-of-band conflicts
By default, manually creating a BGP session whose IP matches a pending
peering request only logs a warning. To make Peering Manager refuse such
creations outright (so an operator cannot accidentally configure a session
while the corresponding request is still awaiting review), enable the
PEERING_REQUEST_BLOCKS_SESSION_CREATION
setting.
Reference Implementation Hints
Peering Portal is the reference implementation, and its source shows each of the calls below in context.
A minimal portal implementation only needs to:
- Settings to provide the Peering Manager URL and API token to use, the
affiliated ASN will be infered from an API call on
/api/peering/portal/affiliated. - A way to surface ASN entry, location selection and session selection to a user.
- A way to display existing peering requests and sessions by using tracking IDs.
All server-side state lives in Peering Manager, so the portal can be deployed as a stateless container.
Endpoint Reference
| Method | Path | Purpose |
|---|---|---|
GET |
/api/peering/portal/affiliated |
Return the affiliated AS bound to the calling token (asn, name) |
GET |
/api/peering/portal/network/{asn} |
Look up a network by ASN in the PeeringDB cache |
GET |
/api/peering/portal/locations?asn={asn}&location_type={public,private} |
List shared IXPs and/or facilities |
POST |
/api/peering/portal/sessions |
Submit a peering request |
GET |
/api/peering/portal/sessions?asn={asn}&request_id={request_id} |
List peering requests filed for an ASN |
GET |
/api/peering/portal/sessions/{request_id} |
Get the status of a request |
DELETE |
/api/peering/portal/sessions/{request_id} |
Cancel a pending request |
All endpoints return JSON. Errors follow Django REST Framework conventions
with a top-level detail field describing the problem.