Menu

Open Payments endpoints

A Dassie node serves Open Payments 1.4.0 as its specs define it: wallet addresses, a GNAP authorization server and a resource server. This page lists the routes and the few things Dassie adds.

To build on these routes, start with Accept payments with Open Payments.

On this page
  1. Where they are served
  2. Wallet addresses
  3. Authorization server
  4. Resource server
  5. Requests
  6. Errors
  7. Dassie's additions
  8. Not supported

Where they are served

Every route is under the origin of one of the node's names, and the node writes that origin into every URL it answers with:

  • A node with a hostname answers under that hostname alone, such as https://pay.example.com.
  • A node without one answers through a front door: at https://<node ID>.dassiepay.net, at the names its owner adds in DASSIE_PUBLIC_NAMES, and at <name>.<domain> for each name it holds, such as https://alice.dassiepay.net. Those domains are dassiepay.net and the domain of each other front door whose <node ID>.<domain> its owner added, up to four front doors in all. Payment names has more.

A front door reaches the node only while the node keeps a connection to it, as its relay. By default, a node without a hostname keeps the dassiepay.net front door, at relay.dassiepay.net. DASSIE_RELAY names another relay, or none. While the node holds no connection to a front door, that front door answers requests for it with 503.

A request for a name the node doesn't answer to gets 421 with the code misdirected_request. Before the node is set up, every route answers 404.

Wallet addresses

Method Path What it answers
GET, HEAD /.well-known/pay The wallet address of the default asset, which the payment pointer $host names
GET, HEAD /pay/<asset> The wallet address of one asset: xrp, dsusd, or usd for the stub in development builds. rlusd belongs to the live realm, which a node refuses
GET, HEAD <wallet address>/jwks.json The key that signs the node's own requests to other wallets, such as /pay/xrp/jwks.json

There is one wallet address per asset the node is paid in. A wallet address names an asset, never a hub or another place the owner keeps money, so where the owner keeps it never shows in a URL. The default asset is the one the owner chose, or else the first active one. Wallet address documents may be cached for 60 seconds, key sets for 300.

Authorization server

Method Path What it does
POST /gnap Asks for a grant
GET /gnap/interact/<id>/<key> Sends the owner on to the wallet's consent page (302). A request that is no longer waiting answers 404 in plain text.
POST /gnap/continue/<id> Continues a grant once the owner has decided
DELETE /gnap/continue/<id> Revokes the grant
POST /gnap/token/<id> Rotates an access token
DELETE /gnap/token/<id> Revokes an access token

Grants for incoming payments and quotes are approved at once. Anything that moves the owner's money or shows more than the client's own resources waits for the owner: outgoing payments, any list-all, read-all on quotes, and a subject request. Reading any incoming payment needs no owner, since a sender's wallet reads one it didn't create to learn how to pay it. A consent request lapses after 10 minutes. Grants and limits covers the rules, such as which limits a grant may set.

Under a front door's name, grants that wait for the owner can't be approved yet: the front door answers /gnap/interact/ itself, with 501 and the code interaction_unavailable, since the owner's app it would send the owner to isn't there yet. They work at a node with a hostname of its own. What's next

Resource server

Method Path What it does
POST /incoming-payments Creates an incoming payment
GET /incoming-payments?wallet-address=… Lists incoming payments, newest first
GET, HEAD /incoming-payments/<id> Reads one. Without a token, anyone gets what it has received and where to get a grant; with a token whose grant may read it, the full payment, and fresh credentials while it still takes money
POST /incoming-payments/<id>/complete Completes it, so it takes no more money
POST /quotes Measures the route to a receiver, and quotes it
GET, HEAD /quotes/<id> Reads a quote
POST /outgoing-payments Creates an outgoing payment, from a quote or from an incoming payment with an amount to debit
GET /outgoing-payments?wallet-address=… Lists outgoing payments, newest first
GET, HEAD /outgoing-payments/<id> Reads one
GET, HEAD /outgoing-payment-grant What the token's grant has spent in the current window of its interval

Each payment method is ilp: STREAM credentials, an ILP address and a shared secret. Every read of an incoming payment hands out fresh credentials, so each sender gets a STREAM connection of its own, and credentials pay one outgoing payment only. An incoming payment counts as paid in full once what it received is within a small tolerance of its amount, so receivedAmount can be a little over incomingAmount. STREAM on Dassie explains why.

Requests

  • Signed as the specs say. Every call the specs protect carries a GNAP token and an HTTP message signature by the client's key. The signature covers the URL at the name the request is answered under, so a request signed for another server never verifies here.
  • Read strictly. Request bodies are JSON (application/json) of at most 16 KiB, and unknown fields are refused where the schemas forbid them.
  • Open to every origin. The routes check no Origin and answer CORS for any origin, without credentials: a client proves itself by its token and signature, never by a cookie. A preflight allows GET, HEAD, POST, DELETE and OPTIONS, and the headers Accept, Authorization, Content-Type, Content-Digest, Content-Length, Signature, Signature-Input and Dassie-Nonce.
  • Rate-limited per client, an IPv4 address or an IPv6 /56: a burst of 120 requests, then one every 250 ms. Requests that create something (grants, incoming payments, quotes and outgoing payments) have a tighter limit on top: a burst of 20, then one every 3 seconds. Past a limit, the node answers 429. Through a front door, the front door limits each client as the node does for all requests, and to a quarter of that for each node: a burst of 30, then one a second. It sets no limit of its own on creating. The node counts the front door as one client, so every client that comes through it shares the node's budget, its limit on creating included.

Errors

An error has the specs' shape, {"error": {"code": "…", "description": "…"}}. The resource server's 401 carries WWW-Authenticate: GNAP as_uri=…, naming the grant endpoint; the authorization server's does not.

Status Code When
400 invalid_request The request is malformed
401 unauthorized At the resource server: no usable token or signature, or a grant that isn't approved
401 invalid_client, invalid_continuation At the authorization server: no usable token or signature, or a continuation the node doesn't know
403 forbidden The grant doesn't allow this
403 invalid_request An outgoing payment the grant's limits don't leave room for, such as one to another receiver than the grant names, or in another asset than its debit limit
404 not_found No such route or resource, or the node isn't set up
408, 413, 415 invalid_request The body took too long, was over 16 KiB, or wasn't JSON
421 misdirected_request The node doesn't answer to that name
429 too_many_requests A rate limit, or too many live grants or quotes from one client
502 unreachable A quote's receiver can't be read, has no route, or its path delivers nothing
503 unavailable The node isn't ready to answer yet

Under a front door's name, the front door answers some requests itself, in the same shape. These answers carry no node signature.

Status Code When
404 not_found No node has that name, or the path isn't an Open Payments route
421 misdirected_request The name isn't under the front door's domain, or names a port other than 443
429 too_many_requests The front door's own rate limit
501 interaction_unavailable A request for /gnap/interact/
502 bad_gateway The node's answer broke, or wasn't JSON
503 unavailable The node holds no connection to the front door, or is busy
504 gateway_timeout The node didn't answer in time

A browser that opens a name at / gets a short page from the front door instead: the name, the node's ID, whether the node is connected to the front door now, and its wallet address.

The authorization server also answers with GNAP's other codes where they fit, such as request_denied, too_fast and invalid_rotation. Error codes also covers the Interledger and STREAM errors a payment can end with.

Dassie's additions

These add to what the specs say, and a client that knows nothing of Dassie can ignore every one of them.

Signed answers

The node signs every answer it gives under a name with its node key, errors included, by RFC 9421. The signature is tagged dassie, its alg is ed25519, and its keyid is the node's two public keys in base64url. It covers @status, the answer's Content-Digest (SHA-512), and the request's @method, @target-uri and Dassie-Nonce. Send a fresh Dassie-Nonce header with your request so an old answer can't be replayed. Only CORS preflights and the refusals of an unknown name or of a node not set up go unsigned. Signed responses and names shows how to check one.

The dassie field

A wallet address document carries "dassie": {"nodeId": "…"}, the ID of the node whose key signs its answers. The specs' schema allows extra properties, so other clients ignore it.

A wallet address per asset

/pay/<asset>, beside /.well-known/pay for the default. Grants and payments record the /pay/<asset> form, so changing the default never moves them to another asset.

One node, several names

Each request is answered under the name it came through, and a name the node doesn't hold gets 421, so whoever passes requests on can't make the node sign for a name its owner never chose.

Not supported

  • SPSP. Wallets that speak only SPSP can't pay a Dassie node. Payment pointers work as another way to write a wallet address.
  • Card authorization: a node has no cards.
  • STREAM receipts, which are not part of Open Payments.

Why it works this way and, for each choice the specs leave open, the payments spec.

A kopje of granite blocks against the sun, in veld grass; one dassie climbs its slab, one keeps lookout on top inside the sun, and one sleeps on the next rock; on a wide screen, a pair sits on a kopje to the left