diff --git a/nixos/modules/issuer.nix b/nixos/modules/issuer.nix
index fdf63d644e61549c8d5791b6584cd3ac32aa6687..2222d834c3aab43638ec9c7333bac7dfe9b4c90a 100644
--- a/nixos/modules/issuer.nix
+++ b/nixos/modules/issuer.nix
@@ -56,6 +56,27 @@ in {
         and payment management.
       '';
     };
+    services.private-storage-issuer.stripeEndpointDomain = lib.mkOption {
+      type = lib.types.str;
+      description = ''
+        The domain name for the Stripe API HTTP endpoint.
+      '';
+      default = "api.stripe.com";
+    };
+    services.private-storage-issuer.stripeEndpointScheme = lib.mkOption {
+      type = lib.types.enum [ "HTTP" "HTTPS" ];
+      description = ''
+        Whether to use HTTP or HTTPS for the Stripe API.
+      '';
+      default = "HTTPS";
+    };
+    services.private-storage-issuer.stripeEndpointPort = lib.mkOption {
+      type = lib.types.int;
+      description = ''
+        The port number for the Stripe API HTTP endpoint.
+      '';
+      default = 443;
+    };
     services.private-storage-issuer.database = lib.mkOption {
       default = "Memory";
       type = lib.types.enum [ "Memory" "SQLite3" ];
@@ -144,7 +165,11 @@ in {
           originStrings = map prefixOption cfg.allowedChargeOrigins;
           originArgs = builtins.concatStringsSep " " originStrings;
 
-          stripeArgs = "--stripe-key-path ${cfg.stripeSecretKeyPath}";
+          stripeArgs =
+            "--stripe-key-path ${cfg.stripeSecretKeyPath} " +
+            "--stripe-endpoint-domain ${cfg.stripeEndpointDomain} " +
+            "--stripe-endpoint-scheme ${cfg.stripeEndpointScheme} " +
+            "--stripe-endpoint-port ${toString cfg.stripeEndpointPort}";
         in
           "${cfg.package}/bin/PaymentServer-exe ${originArgs} ${issuerArgs} ${databaseArgs} ${httpsArgs} ${stripeArgs}";
     };
diff --git a/nixos/modules/tests/get-passes.py b/nixos/modules/tests/get-passes.py
index d76ce3ccbf703bfabd08e120f84ed39063339297..96875713233b7c46abae0cfc3f0d946628bf8cc2 100755
--- a/nixos/modules/tests/get-passes.py
+++ b/nixos/modules/tests/get-passes.py
@@ -12,23 +12,25 @@ from json import dumps
 from time import sleep
 
 def main():
-    clientAPIRoot, issuerAPIRoot = argv[1:]
+    if len(argv) != 4:
+        raise SystemExit(
+            "usage: %s <client api root> <issuer api root> <voucher>",
+        )
+    clientAPIRoot, issuerAPIRoot, voucher = argv[1:]
     if not clientAPIRoot.endswith("/"):
         clientAPIRoot += "/"
     if not issuerAPIRoot.endswith("/"):
         issuerAPIRoot += "/"
 
-    # Construct a voucher that's acceptable to various parts of the system.
-    voucher = "a" * 44
-
     zkapauthz = clientAPIRoot + "storage-plugins/privatestorageio-zkapauthz-v1"
 
-    # Simulate a payment for a voucher.
-    post(
-        issuerAPIRoot + "v1/stripe/webhook",
-        dumps(charge_succeeded_json(voucher)),
+    # Submit a charge to the issuer (which is also the PaymentServer).
+    charge_response = post(
+        issuerAPIRoot + "v1/stripe/charge",
+        dumps(charge_json(voucher)),
         headers={"content-type": "application/json"},
     )
+    charge_response.raise_for_status()
 
     # Tell the client to redeem the voucher.
     response = put(
@@ -64,141 +66,13 @@ def retry(description, f):
     raise ValueError("failed to {} after many tries".format(description))
 
 
-def charge_succeeded_json(voucher):
-    # This structure copy/pasted from Stripe webhook web interface.
-    base_payload = {
-        "id": "evt_1FKSX2DeTd13VRuuhPaUDA2f",
-        "object": "event",
-        "api_version": "2016-07-06",
-        "created": 1568910660,
-        "data": {
-            "object": {
-                "id": "ch_1FKSX2DeTd13VRuuG9BXbqji",
-                "object": "charge",
-                "amount": 999,
-                "amount_refunded": 0,
-                "application": None,
-                "application_fee": None,
-                "application_fee_amount": None,
-                "balance_transaction": "txn_1FKSX2DeTd13VRuuqO1CJZ1e",
-                "billing_details": {
-                    "address": {
-                        "city": None,
-                        "country": None,
-                        "line1": None,
-                        "line2": None,
-                        "postal_code": None,
-                        "state": None
-                    },
-                    "email": None,
-                    "name": None,
-                    "phone": None
-                },
-                "captured": True,
-                "created": 1568910660,
-                "currency": "usd",
-                "customer": None,
-                "description": None,
-                "destination": None,
-                "dispute": None,
-                "failure_code": None,
-                "failure_message": None,
-                "fraud_details": {
-                },
-                "invoice": None,
-                "livemode": False,
-                "metadata": {
-                    "Voucher": None,
-                },
-                "on_behalf_of": None,
-                "order": None,
-                "outcome": {
-                    "network_status": "approved_by_network",
-                    "reason": None,
-                    "risk_level": "normal",
-                    "risk_score": 44,
-                    "seller_message": "Payment complete.",
-                    "type": "authorized"
-                },
-                "paid": True,
-                "payment_intent": None,
-                "payment_method": "card_1FKSX2DeTd13VRuus5VEjmjG",
-                "payment_method_details": {
-                    "card": {
-                        "brand": "visa",
-                        "checks": {
-                            "address_line1_check": None,
-                            "address_postal_code_check": None,
-                            "cvc_check": None
-                        },
-                        "country": "US",
-                        "exp_month": 9,
-                        "exp_year": 2020,
-                        "fingerprint": "HTJeRR4MXhAAkctF",
-                        "funding": "credit",
-                        "last4": "4242",
-                        "three_d_secure": None,
-                        "wallet": None
-                    },
-                    "type": "card"
-                },
-                "receipt_email": None,
-                "receipt_number": None,
-                "receipt_url": "https://pay.stripe.com/receipts/acct_198xN4DeTd13VRuu/ch_1FKSX2DeTd13VRuuG9BXbqji/rcpt_Fq8oAItSiNmcm0beiie6lUYin920E7a",
-                "refunded": False,
-                "refunds": {
-                    "object": "list",
-                    "data": [
-                    ],
-                    "has_more": False,
-                    "total_count": 0,
-                    "url": "/v1/charges/ch_1FKSX2DeTd13VRuuG9BXbqji/refunds"
-                },
-                "review": None,
-                "shipping": None,
-                "source": {
-                    "id": "card_1FKSX2DeTd13VRuus5VEjmjG",
-                    "object": "card",
-                    "address_city": None,
-                    "address_country": None,
-                    "address_line1": None,
-                    "address_line1_check": None,
-                    "address_line2": None,
-                    "address_state": None,
-                    "address_zip": None,
-                    "address_zip_check": None,
-                    "brand": "Visa",
-                    "country": "US",
-                    "customer": None,
-                    "cvc_check": None,
-                    "dynamic_last4": None,
-                    "exp_month": 9,
-                    "exp_year": 2020,
-                    "fingerprint": "HTJeRR4MXhAAkctF",
-                    "funding": "credit",
-                    "last4": "4242",
-                    "metadata": {
-                    },
-                    "name": None,
-                    "tokenization_method": None
-                },
-                "source_transfer": None,
-                "statement_descriptor": None,
-                "statement_descriptor_suffix": None,
-                "status": "succeeded",
-                "transfer_data": None,
-                "transfer_group": None
-            }
-        },
-        "livemode": False,
-        "pending_webhooks": 2,
-        "request": "req_5ozjOIAcOkvVUK",
-        "type": "charge.succeeded"
+def charge_json(voucher):
+    return {
+        "token": "tok_abcdef",
+        "voucher": voucher,
+        "amount": "100",
+        "currency": "USD",
     }
-    # Indicate the voucher the payment references.
-    base_payload["data"]["object"]["metadata"]["Voucher"] = voucher
-    return base_payload
-
 
 if __name__ == '__main__':
     main()
diff --git a/nixos/modules/tests/private-storage.nix b/nixos/modules/tests/private-storage.nix
index 9028691fc8edb133b5c048291cd5f0741f6f4ea1..1f6d40cb30ebe9f953c882d428746f2593190d36 100644
--- a/nixos/modules/tests/private-storage.nix
+++ b/nixos/modules/tests/private-storage.nix
@@ -9,9 +9,16 @@ let
   get-passes = ./get-passes.py;
   exercise-storage = ./exercise-storage.py;
 
+  # This is a test double of the Stripe API server.  It is extremely simple.
+  # It barely knows how to respond to exactly the API endpoints we use,
+  # exactly how we use them.
+  stripe-api-double = ./stripe-api-double.py;
+
   # The root URL of the Ristretto-flavored PrivacyPass issuer API.
   issuerURL = "http://issuer/";
 
+  voucher = "xyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxy";
+
   # The issuer's signing key.  Notionally, this is a secret key.  This is only
   # the value for this system test though so I don't care if it leaks to the
   # world at large.
@@ -42,6 +49,22 @@ let
     networking.firewall.enable = false;
     networking.dhcpcd.enable = false;
   };
+
+  # Return a Perl program fragment to run a shell command on one of the nodes.
+  # The first argument is the name of the node.  The second is a list of the
+  # argv to run.
+  #
+  # The program's output is piped to systemd-cat and the Perl fragment
+  # evaluates to success if the command exits with a success status.
+  runOnNode = node: argv:
+    let
+      command = builtins.concatStringsSep " " argv;
+    in
+      "
+      \$${node}->succeed('set -eo pipefail; ${command} | systemd-cat');
+      # succeed() is not success but 1 is.
+      1;
+      ";
 in
 # https://nixos.org/nixos/manual/index.html#sec-nixos-tests
 import <nixpkgs/nixos/tests/make-test.nix> {
@@ -69,11 +92,13 @@ import <nixpkgs/nixos/tests/make-test.nix> {
       { imports =
         [ ../private-storage.nix
         ];
-        services.private-storage.enable = true;
-        services.private-storage.publicIPv4 = "storage";
-        services.private-storage.introducerFURL = introducerFURL;
-        services.private-storage.issuerRootURL = issuerURL;
-        services.private-storage.ristrettoSigningKeyPath = pkgs.writeText "signing-key.private" ristrettoSigningKey;
+        services.private-storage = {
+          enable = true;
+          publicIPv4 = "storage";
+          introducerFURL = introducerFURL;
+          issuerRootURL = issuerURL;
+          inherit ristrettoSigningKeyPath;
+        };
       } // networkConfig;
 
     # Operate an issuer as well.
@@ -90,8 +115,28 @@ import <nixpkgs/nixos/tests/make-test.nix> {
         inherit ristrettoSigningKey;
         stripeSecretKeyPath = pkgs.writeText "stripe.secret" stripeSecretKey;
         letsEncryptAdminEmail = "user@example.invalid";
+        stripeEndpointDomain = "api_stripe_com";
+        stripeEndpointScheme = "HTTP";
+        stripeEndpointPort = 80;
       };
     } // networkConfig;
+
+    "api_stripe_com" =
+    { config, pkgs, ... }:
+      let python = pkgs.python3.withPackages (ps: [ ps.twisted ]);
+      in networkConfig // {
+        environment.systemPackages = [
+          python
+          pkgs.curl
+        ];
+
+        systemd.services."api.stripe.com" = {
+          enable = true;
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" ];
+          script = "${python}/bin/python ${stripe-api-double} tcp:80";
+        };
+      };
   };
 
   # Test the machines with a Perl program (sobbing).
@@ -107,13 +152,7 @@ import <nixpkgs/nixos/tests/make-test.nix> {
       );
 
       eval {
-        $introducer->succeed(
-          'set -eo pipefail; ' .
-          '${run-introducer} /tmp/node.pem ${toString introducerPort} ${introducerFURL} | ' .
-          'systemd-cat'
-        );
-        # Signal success. :/
-        1;
+      ${runOnNode "introducer" [ run-introducer "/tmp/node.pem" (toString introducerPort) introducerFURL ]}
       } or do {
         my $error = $@ || 'Unknown failure';
         my ($code, $log) = $introducer->execute('cat /tmp/stdout /tmp/stderr');
@@ -142,26 +181,32 @@ import <nixpkgs/nixos/tests/make-test.nix> {
       #
       # Storage appears to be working so try to get a client to speak with it.
       #
-      $client->succeed('set -eo pipefail; ${run-client} ${introducerFURL} ${issuerURL} | systemd-cat');
+      ${runOnNode "client"  [ run-client introducerFURL issuerURL ]}
       $client->waitForOpenPort(3456);
 
       # Get some ZKAPs from the issuer.
       eval {
-        $client->succeed('set -eo pipefail; ${get-passes} http://127.0.0.1:3456 ${issuerURL} | systemd-cat');
-        # succeed() is not success but 1 is.
+        $api_stripe_com->waitForUnit("api.stripe.com");
         1;
+      } or do {
+        my ($code, $log) = $api_stripe_com->execute('journalctl -u api.stripe.com');
+        $api_stripe_com->log($log);
+        die $@;
+      };
+      eval {
+        ${runOnNode "client" [ get-passes "http://127.0.0.1:3456" issuerURL voucher ]}
       } or do {
         my $error = $@ || 'Unknown failure';
         my ($code, $log) = $client->execute('cat /tmp/stdout /tmp/stderr');
         $client->log($log);
+        my ($code, $log) = $api_stripe_com->execute('journalctl -u api.stripe.com');
+        $api_stripe_com->log($log);
         die $@;
       };
 
       # The client should be prepped now.  Make it try to use some storage.
       eval {
-        $client->succeed('set -eo pipefail; ${exercise-storage} /tmp/client | systemd-cat');
-        # nothing succeeds like ... 1.
-        1;
+        ${runOnNode "client" [ exercise-storage "/tmp/client" ]}
       } or do {
         my $error = $@ || 'Unknown failure';
         my ($code, $log) = $client->execute('cat /tmp/stdout /tmp/stderr');
diff --git a/nixos/modules/tests/stripe-api-double.py b/nixos/modules/tests/stripe-api-double.py
new file mode 100755
index 0000000000000000000000000000000000000000..5b96c0befe2a9434560bdd357f3d8e8dc2a92bc5
--- /dev/null
+++ b/nixos/modules/tests/stripe-api-double.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python3
+
+from sys import stdout, argv
+from json import dumps
+
+from twisted.internet.defer import Deferred
+from twisted.internet.endpoints import serverFromString
+from twisted.internet.task import react
+from twisted.web.resource import Resource
+from twisted.web.server import Site
+from twisted.python.log import startLogging
+
+class Charges(Resource):
+    def render_POST(self, request):
+        voucher = request.args[b"metadata[Voucher]"][0].decode("utf-8")
+        card = request.args[b"card"][0].decode("utf-8")
+        amount = int(request.args[b"amount"][0])
+        currency = request.args[b"currency"][0].decode("utf-8")
+        response = dumps(charge(card, amount, currency, {u"Voucher": voucher}))
+        return response.encode("utf-8")
+
+def main(reactor, listenEndpoint):
+    charges = Charges()
+    v1 = Resource()
+    v1.putChild(b"charges", charges)
+    root = Resource()
+    root.putChild(b"v1", v1)
+
+    return serverFromString(reactor, listenEndpoint).listen(
+        Site(root),
+    ).addCallback(
+        lambda ignored: Deferred()
+    )
+
+def charge(source, amount, currency, metadata):
+    return {
+        "id": "ch_1Fj8frBHXBAMm9bPkekylvAq",
+        "object": "charge",
+        "amount": amount,
+        "amount_refunded": 0,
+        "application": None,
+        "application_fee": None,
+        "application_fee_amount": None,
+        "balance_transaction": "txn_1Fj8fr2eZvKYlo2CC5JzIGj5",
+        "billing_details": {
+            "address": {
+                "city": None,
+                "country": None,
+                "line1": None,
+                "line2": None,
+                "postal_code": None,
+                "state": None
+            },
+            "email": None,
+            "name": None,
+            "phone": None
+        },
+        "captured": False,
+        "created": 1574792527,
+        "currency": currency,
+        "customer": None,
+        "description": None,
+        "dispute": None,
+        "disputed": False,
+        "failure_code": None,
+        "failure_message": None,
+        "fraud_details": {},
+        "invoice": None,
+        "livemode": False,
+        "metadata": metadata,
+        "on_behalf_of": None,
+        "order": None,
+        "outcome": None,
+        "paid": True,
+        "payment_intent": None,
+        "payment_method": source,
+        "payment_method_details": {},
+        "receipt_email": None,
+        "receipt_number": None,
+        "receipt_url": "https://pay.stripe.com/receipts/acct_1FhhxTBHXBAMm9bP/ch_1Fj8frBHXBAMm9bPkekylvAq/rcpt_GFdxYuDoGKfYgokh9YA11XhnYC7Gnxp",
+        "refunded": False,
+        "refunds": {
+            "object": "list",
+            "data": [],
+            "has_more": False,
+            "url": "/v1/charges/ch_1Fj8frBHXBAMm9bPkekylvAq/refunds"
+        },
+        "review": None,
+        "shipping": None,
+        "source_transfer": None,
+        "statement_descriptor": None,
+        "statement_descriptor_suffix": None,
+        "status": "succeeded",
+        "transfer_data": None,
+        "transfer_group": None,
+        "source": source,
+    }
+
+if __name__ == '__main__':
+    startLogging(stdout)
+    react(main, argv[1:])
diff --git a/nixos/pkgs/zkapissuer-repo.nix b/nixos/pkgs/zkapissuer-repo.nix
index d31233b41997a94b424a1eacf47d92423768c3d6..a252d3a9b31cc83ef879e1a1f9561f0480860d1f 100644
--- a/nixos/pkgs/zkapissuer-repo.nix
+++ b/nixos/pkgs/zkapissuer-repo.nix
@@ -4,6 +4,6 @@ in
   pkgs.fetchFromGitHub {
     owner = "PrivateStorageio";
     repo = "PaymentServer";
-    rev = "e1e0817b80ce1031d21587357bfb8dc4e7837b01";
-    sha256 = "0i95cpqbz9fj4b1cii1xh01ss77v2jgd5qwzqvk812f0slz3fw6q";
+    rev = "1130b17e85392efd9f6be733308542b50bded1e3";
+    sha256 = "1ivcy3xcakxs0yfvbnvizq9pchp15g2wdprh5r5rq4fkqk8k6nbf";
   }
\ No newline at end of file