Validating PayPal Webhooks Offline (Almost)

PayPal offers the abillity for you to receive webhooks for transaction notifications.  This isn’t exactly new — it was introduced with the REST APIs back in 2013(-ish?).  But for those of you still using IPN, you should know that webhooks has some big advantages over IPN.

First, webhooks provides a more structured way to find out exactly what happened.  Each webhook event includes an event_type — so you can figure out just by looking at that what happened.  Second, PayPal provides APIs to let you create webhooks, retrieve events, replay events, and even see samples of the different event types.  Third, you can have more than one webhook per account — this is a big advantage over IPN, which would only let you have one IPN listener per account.  There are more advantages, but that’s not what I want to focus on for this post.

As with IPN, there’s the question of “how do I know that this webhook event is genuine?”  PayPal has the Verify Webhook Signature API to do this — but what if I want to do it without making another API call?  There is actually a way to do this.

PayPal crytographically signs the webhook event when it’s sent to you — and (almost) all the information that you need to verify the signature (as well as the signature itself) are included in the HTTP post.  Let’s look at the different elements:

First, there are a number of HTTP headers that PayPal includes when it makes the post to your site:

  • PAYPAL-TRANSMISSION-ID is a unique ID (more specifically, a UUID) for the transmission.
  • PAYPAL-TRANSMISSION-TIME is the time when PayPal initiated the transmission of the webhook, in ISO 8601 format.
  • PAYPAL-TRANSMISSION-SIG is the Base64-encoded signature.
  • PAYPAL-CERT-URL is the URL to the certificate which corresponds to the private key that was used to generate the signature.
  • PAYPAL-AUTH-ALGO is the algorithm that was used to generate the signature.  (I’ve only ever seen PayPal use SHA256withRSA, but it’s possible that PayPal might switch in the future if/when SHA256 is broken.)

And lastly, there’s the body of the HTTP post itself — the webhook JSON.

How do you validate the signature?  Well, the signature isn’t based off the body of the webhook itself; rather, it’s based off the following string:

<transmissionId>|<timestamp>|<webhookId>|<crc>

  • <transmissionId> and <timestamp> are the verbatim values given in the PAYPAL-TRANSMISSION-ID and PAYPAL-TRANSMISSION-TIME HTTP headers, respectively.
  • <webhookId> is the ID that PayPal assigned to your webhook when you created it.  You can find this a few different places:
    • If you used the Webhooks API to create the webhook, this would have been the value of /id in the response.
    • You can use the List All Webhooks API to see the webhooks you have registered.  You can grab the webhook ID from there.
    • You can also see your webhooks from developer.paypal.com.  (Go to the Dashboard, then the My Apps & Credentials page.  Scroll down to the REST API Apps section and find your REST app.  Click on it, then scroll down to the “Sandbox Webhooks” or “Live Webhooks” section.  The webhook ID will be displayed in the “Webhook ID” column.)
  • <crc> is the CRC32 of the body of the HTTP post (e.g., the raw, unaltered webhook JSON), and expressed as a base 10, unsigned integer.

Let’s look at a quick example.  Suppose this is what PayPal posted to you.  (This is an actual webhook I received, albeit slightly modified:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /paypal-webhook-handler HTTP/1.1
Accept: */*
PAYPAL-TRANSMISSION-ID: 6e3b26a0-9287-11e7-ac1e-6b62a8a99ac4
PAYPAL-TRANSMISSION-TIME: 2017-09-05T22:13:22Z
PAYPAL-TRANSMISSION-SIG: Hdwao5lBJ9R6IX1JgOuyKdA1oyw2edUGhJ4ovHDqA7XXJS9BvVMQJL/51nXzVu5mI0iDTfkXk8XophZnkXB+srwtdxkjjIeW+fNMsp9qsI64gywFK40AqD6YvyIbbBhGm8SPecfVGOWYeAy16jHx/6F6e/wxeSClM8XcQMrp6jwy5NZRyD/0BsijjI6KQedonrg6jiq3BqrzbvIyuMW32DtiqXPg/2Inog0ZItpTmHDu71Xci6zgiTmb4BsKHX/vyBwRZE6wo4NwtiP1NoNr+l32H3JCAvOvjvPRBAFbaG+SKjUGn3NL8nV3EQGXV20rJI4l5wWRYh5C4DBzppXgkA==
PAYPAL-AUTH-VERSION: v2
PAYPAL-CERT-URL: https://api.sandbox.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-aecacc47
PAYPAL-AUTH-ALGO: SHA256withRSA
Content-Type: application/json
User-Agent: PayPal/AUHD-211.0-33754056
Host: www.bahjeez.com
correlation-id: 42e699ec204cc
CAL_POOLSTACK: amqunphttpdeliveryd:UNPHTTPDELIVERY*CalThreadId=0*TopLevelTxnStartTime=15e541b2066*Host=slcsbamqunphttpdeliveryd3002
CLIENT_PID: 21282
Content-Length: 965

{"id":"WH-36687761JL817053T-6SY78077XN391202M","event_version":"1.0","create_time":"2017-09-05T22:13:22.000Z","resource_type":"payouts","event_type":"PAYMENT.PAYOUTSBATCH.SUCCESS","summary":"Payouts batch completed successfully.","resource":{"batch_header":{"payout_batch_id":"2AZEQUD4YPAEJ","batch_status":"SUCCESS","time_created":"2017-09-05T22:12:56Z","time_completed":"2017-09-05T22:13:22Z","sender_batch_header":{"sender_batch_id":"2017021897"},"amount":{"currency":"USD","value":"1.0"},"fees":{"currency":"USD","value":"0.0"},"payments":1},"links":[{"href":"https://api.sandbox.paypal.com/v1/payments/payouts/2AZEQUD4YPAEJ","rel":"self","method":"GET"}]},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-36687761JL817053T-6SY78077XN391202M","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-36687761JL817053T-6SY78077XN391202M/resend","rel":"resend","method":"POST"}]}

In this example:

  • <transmissionId> would be 6e3b26a0-9287-11e7-ac1e-6b62a8a99ac4.
  • <timestamp> would be 2017-09-05T22:13:22Z.  (Remember — use the exact value that PayPal passed to you.  Don’t try to change this into your local timezone or change its format.)
  • <id> would be my webhook ID, which in this case is 2R269424P6803053B.
  • <crc> would be 1330495958.

Which means that the string PayPal signed would be:

6e3b26a0-9287-11e7-ac1e-6b62a8a99ac4|2017-09-05T22:13:22Z|2R269424P6803053B|1330495958

The last thing to do is to verify the signature.

So far, we’ve been able to do everything without pulling in any external resources, but unfortunately that ends here.  To verify the signature, we need a copy of the certificate that corresponds to the private key that was used to generate the signature.  PayPal provided us a URL where we can fetch that certificate (in the PAYPAL-CERT-URL header) — we’ll need to fetch a copy of that.  Bad news is that means pulling in an outside resource (which will slow down the verification process); good news is that the certificates don’t change that often (in fact, I’ve only ever seen PayPal use one certificate), so you can cache the certificate for future use.

The only thing that’s left is to verify the signature against the string we formed above.  I won’t get into specifics on this — each language has their own way of pulling this off.  Java has built-in classes and methods that will help you out with this; for PHP, you can use the built-in OpenSSL functions to help you out.

If the signature verification is successful, and you trust the certificate that was used to sign the message, then you can be sure that the message you’re receiving is genuine.

Side note: there’s a weakness here in that CRC32 is used to hash the actual message body.  CRC32 isn’t a secure hashing algorithm (not sure it was ever meant to be), so I’m not sure why PayPal decided to use that instead of something like SHA256.  (Edit: I’m told something new is in the works.)

Anywho…I wrote a couple of example implementations.  Note that these examples don’t cache the certificates — you’ll need to figure out how to do that on your own.  But, feel free to use what I have.

First, a PHP example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<?php

$headers = apache_request_headers();

$cert_url = $headers[ 'PAYPAL-CERT-URL' ];
$transmission_id = $headers[ 'PAYPAL-TRANSMISSION-ID' ];
$timestamp = $headers[ 'PAYPAL-TRANSMISSION-TIME' ];
$algo = $headers[ 'PAYPAL-AUTH-ALGO' ];
$signature = $headers[ 'PAYPAL-TRANSMISSION-SIG' ];
$webhook_id = "09A5628866464184S"; // Replace with your webhook ID

$webhook_body = file_get_contents( 'php://input' );

try {
  if( verify_webhook( $cert_url, $transmission_id, $timestamp, $webhook_id, $algo, $signature, $webhook_body ) ) {
    // Verification succeeded!
  } else {
    // Verification failed!
  }
} catch(Exception $ex) {
  // Something went wrong during verification!
}

/**
 * Verifies a webhook from PayPal.
 *
 * @param string $cert_url The URL of the certificate that corresponds to the
 *                         private key that was used to sign the certificate.
 *                         When the webhook is posted to you, PayPal provides
 *                         this in the PAYPAL-CERT-URL HTTP header.
 * @param string $transmission_id The transmission ID for the webhook event.
 *                                When the webhook is posted to you, PayPal
 *                                provides this in the PAYPAL-TRANSMISSION-ID
 *                                HTTP header.
 * @param string $timestamp The timestamp of when the webhook was sent. When
 *                          the webhook is posted to you, PayPal provides
 *                          this in the PAYPAL-TRANSMISSION-TIME HTTP header.
 * @param string $webhook_id The webhook ID assigned to your webhook, as
 *                           defined in your developer.paypal.com dashboard.
 *                           If you used the Create Webhook API to create your
 *                           webhook, this ID was returned in the response to
 *                           that call.
 * @param string $signature_algorithm The signature algorithm that was used to
 *                                    generate the signature for the webhook.
 *                                    When the webhook is posted to you, PayPal
 *                                    provides this in the PAYPAL-AUTH-ALGO
 *                                    HTTP header.
 * @param string $webhook_body The byte-for-byte body of the request that
 *                             PayPal posted to you.
 *
 * @return bool Returns true if the webhook could be successfully verified, or
 *              false if it was not.
 *
 * @throws Exception if an error occurred while attempting to verify the
 *     webhook.
 */

function verify_webhook( $cert_url, $transmission_id, $timestamp, $webhook_id, $signature_algorithm, $signature, $webhook_body ) {
  // This is used to translate the hash methods provided by PayPal into ones that
  // are known by OpenSSL...right now the only one we've seen PayPal use is 'SHA256withRSA'
  $known_hash_methods = [
    'SHA256withRSA' =&gt; 'sha256WithRSAEncryption'
  ];

  if( array_key_exists( $signature_algorithm, $known_hash_methods ) ) {
    $algo = $known_hash_methods[ $signature_algorithm ];
  } else {
    $algo = $signature_algorithm;
  }

  // Make sure OpenSSL knows how to handle this hash method
  $openssl_algos = openssl_get_md_methods( true );
  if( !in_array( $algo, $openssl_algos ) ) {
    throw new Exception( "OpenSSL doesn't know how to handle message digest algorithm "$algo"" );
  }

  // Fetch the cert -- we have to use cURL for this because PHP's built-in
  // capability for opening http/https URLs uses HTTP 1.0, which PayPal doesn't
  // support
  $curl = curl_init( $cert_url );
  curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
  $cert = curl_exec( $curl );

  if( false === $cert ) {
    $error = curl_error( $curl );
    curl_close( $curl );
    throw new Exception( "Failed to fetch certificate from server: $error" );
  }

  curl_close( $curl );

  // Parse the certificate
  $x509 = openssl_x509_read( $cert );
  if( false === $x509 ) {
    throw new Exception( "OpenSSL was unable to parse the certificate from PayPal\n" );
  }

  // Calculate the CRC32 of the webhook body
  $crc = crc32( $webhook_body );

  // Assemble the string that PayPal actually signed
  $sig_string = sprintf( '%s|%s|%s|%u', $transmission_id, $timestamp, $webhook_id, $crc );

  // Base64-decode PayPal's signature
  $decoded_signature = base64_decode( $signature );

  // Fetch the public key from the certificate
  $pkey = openssl_pkey_get_public( $cert );
  if( false === $pkey ) {
    throw new Exception( "Failed to get public key from PayPal certificate\n" );
  }

  // Verify the signature
  $verify_status = openssl_verify( $sig_string, $decoded_signature, $pkey, $algo );

  openssl_x509_free( $x509 );

  // Check the status of the verification
  if( $verify_status == 1 ) {
    return true;
  } else if( $verify_status == -1 ) {
    throw new Exception( "Error occurred while trying to verify webhook signature" );
  } else {
    return false;
  }
}

And second, a Java servlet (written for Apache Tomcat 8):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package com.bahjeez;

import java.io.IOException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.stream.Collectors;
import java.util.zip.CRC32;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class ValidateWebhook
 */

@WebServlet(name = "ValidateWebhook", urlPatterns = { "/ValidateWebhook" })
public class ValidateWebhook extends HttpServlet {
    private static final long serialVersionUID = 1L;
       
    /**
     * @see HttpServlet#HttpServlet()
     */

    public ValidateWebhook() {
        super();
    }
   
    public static boolean verifySignature(String webhookBody, String certUrl, String transmissionId, String transmissionTimestamp, String authAlgo, String signature, String webhookId) throws Exception {
       
        CertificateFactory fact;
        try {
            fact = CertificateFactory.getInstance("X.509");
        } catch (CertificateException e) {
            throw new Exception("Failed to construct CertificateFactory object");
        }
       
        URL url = new URL(certUrl);
        X509Certificate cer;
        try {
            cer = (X509Certificate) fact.generateCertificate(url.openStream());
        } catch (CertificateException e) {
            throw new Exception("Failed to create X509Certificate object");
        }
       
        Signature sigAlgo;
        try {
            sigAlgo = Signature.getInstance(authAlgo);
        } catch (NoSuchAlgorithmException e) {
            throw new Exception("Failed to initialize Signature object (maybe unrecognized signature algorithm?)");
        }
       
        CRC32 crc = new CRC32();
        crc.update(webhookBody.getBytes());
       
        String verifyString = transmissionId + "|" + transmissionTimestamp + "|" + webhookId + "|" + crc.getValue();
       
        try {
            sigAlgo.initVerify(cer);
        } catch (InvalidKeyException e) {
            throw new Exception("Failed to initialize signature verification");
        }
       
        try {
            sigAlgo.update(verifyString.getBytes());
        } catch (SignatureException e) {
            throw new Exception("Failed to update signature verification object");
        }
       
        byte[] actualSignature = Base64.getDecoder().decode(signature);
        try {
            return sigAlgo.verify(actualSignature);
        } catch (SignatureException e) {
            throw new Exception("Failed to verify signature");
        }
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String webhookBody = request.getReader().lines().collect(Collectors.joining());
        String certUrl = request.getHeader("PAYPAL-CERT-URL");
        String transmissionId = request.getHeader("PAYPAL-TRANSMISSION-ID");
        String transmissionTimestamp = request.getHeader("PAYPAL-TRANSMISSION-TIME");
        String authAlgo = request.getHeader("PAYPAL-AUTH-ALGO");
        String signature = request.getHeader("PAYPAL-TRANSMISSION-SIG");
        String webhookId = "24N36863A45710219";
       
        try {
            if(this.verifySignature(webhookBody, certUrl, transmissionId, transmissionTimestamp, authAlgo, signature, webhookId)) {
                response.setStatus(200);
            } else {
                response.setStatus(400);
                response.getWriter().write("Failed to verify signature on incoming webhook");
            }
        } catch(Exception ex) {
            response.setStatus(500);
            response.getWriter().write("Failed to verify signature due to internal error: " + ex.getMessage());
        }
    }

}

Recurring Payments IPNs

I originally posted this article to x.com on August 3, 2010. Since that time, x.com has been repurposed, and my posts have been taken down. I have reposted this here for informational and historical purposes.

(Updated 8/9/2010 to include recurring_payment_expired)

There are several different values for the txn_type variable in an IPN message that are related to Recurring Payments:

  • recurring_payment
  • recurring_payment_failed
  • recurring_payment_expired
  • recurring_payment_suspended_due_to_max_failed_payment
  • recurring_payment_profile_created
  • recurring_payment_profile_cancel
  • recurring_payment_outstanding_payment_failed
  • recurring_payment_outstanding_payment
  • recurring_payment_skipped

But, what do they all mean?

At one point in time, one of my merchants asked me this same question.  I had a hell of a time (pardon my French) finding the answer — especially recurring_payment_skipped, which seems to be a pain point for many developers.  Eventually, through talking to people internally and doing test cases, I managed to find the answers.  I’ve had enough people ask me this same question that having the answer sitting around has been extremely useful.

  • When the recurring payments profile is created, you receive an IPN with txn_type set to recurring_payment_profile_created.
  • For each successful payment, you receive an IPN with txn_type set to recurring_payment.
  • For each unsuccessful payment, you receive an IPN with txn_type set to recurring_payment_failed. The outstanding_balance field will have the amount currently outstanding.
  • When the maximum number of failed payments is reached (as specified in the MAXFAILEDPAYMENTS parameter in your CreateRecurringPaymentsProfile call), you receive an IPN with txn_type set to recurring_payment_suspended_due_to_max_failed_payment. This is the only IPN you receive (e.g., if MAXFAILEDPAYMENTS was set to 1, you only receive this IPN on the failed payment; you do not receive another one with txn_type of recurring_payment_failed).
  • If the recurring payments profile is cancelled, you receive an IPN with txn_type set to recurring_payment_profile_cancel.
  • When the profile has “expired” (e.g., there are no more payments left on the profile, and the amount of time since the last payment plus the billing period has elapsed), you will get another IPN with txn_type set to recurring_payment_expired.  This is intended to be a “reminder” to you that the buyer’s subscription is up, and to deactivate their service.  For example, if you create a recurring payments profile on the 16th of the month, with a billing period of one month, and the profile is cancelled on the 28th of the month, you will get an IPN on the 28th with txn_type of recurring_payment_profile_cancel, and another IPN on the 16th of the next month with recurring_payment_expired.

    Edit 3/8/2011: It’s been brought to my attention that recurring_payment_expired is only sent for Website Payments Standard subscriptions.  I’m not entirely clear on all the details, but it appears that newer profiles (ones where the subscription ID starts with “I-“) will send recurring_payment_expired, whereas older profiles (ones where the subscription ID starts with “S-“) will send subscr_eot.

  • If you call BillOutstandingAmount, the resulting IPN will have txn_type set to either recurring_payment_outstanding_payment_failed or recurring_payment_outstanding_payment. (Remember that if the profile has been suspended, you will need to call ManageRecurringPaymentsProfileStatus to reactivate the profile.)
  • When you receive an IPN with txn_type set to recurring_payment_skipped, this means that, for some reason, PayPal was not able to process the recurring payment.  This does not necessarily mean that the buyer’s credit card (or other funding source) was declined, but rather, it indicates that some other error occurred that prevented us from processing the payment.  Because there are multiple reasons why this could happen, PayPal will make three attempts to charge the buyer — once after three days, and again five days after that.  If the 3-day reattempt fails you will receive another IPN with txn_type set to recurring_payment_skipped.  If the 5-day reattempt fails, the payment will be considered a failed payment, and you will receive an IPN with txn_type set to either recurring_payment_failed or recurring_payment_suspended_due_to_max_failed_payment, depending on how you set up the profile.

International Address Formats

I originally posted this article to x.com on September 21, 2010. Since that time, x.com has been repurposed, and my posts have been taken down. I have reposted this here for informational and historical purposes.

I know, both first and second-hand, that there is a lot of confusion over exactly what data needs to be passed for addresses in foreign countries.  In this post, I’ll try to lay out each country’s specific rules.  This is going to be a work-in-progress, as I don’t anticipate having every country shown on this list right off the bat, but I hope that I’m doing some good by putting it out there.

Please be aware that much of this information was gathered from internal sources, and although I believe it to be correct, I haven’t had time to test out every country to make sure that it’s 100% correct.  So please, if you find something here that turns out to be incorrect, please let me know so that I can get it fixed!  (I’d rather be right than wrong any day!)  Just leave a comment at the bottom of the page.

Any time I make reference to fields such as STREET, STREET2, CITY, STATE, ZIP, and COUNTRYCODE, please assume that they will correspond to the fields for the specific API call and language (NVP or SOAP) that you are using.

Also, any time I specify the format for ZIP, N should be a numeric character (0-9), and X should be an alphabetic character (A-Z).

Lastly, you may not be required to pass any address information at all.  If a particular country is giving you problems, try running an API call without passing any address information at all — if the transaction succeeds, then you can work around the issue by simply not passing the address.

P.S. — There’s been some attempts before to compile information like this.  See here (link broken) and here (link broken) for more information.  There’s also a list of country codes (we use the ISO 3166-1 alpha-2 codes) here or here.

Australia

  • You need to pass STREET, CITY, and ZIPSTATE is optional.  If you do supply a STATE, it should be the full state name (e.g., Northern Territory).
  • ZIP should be specified as NNNN.

Canada

  • You need to pass STREET, CITY, STATE, and ZIP.
  • STATE should be set to the province’s two-character abbreviation (ex.: QC).
  • ZIP should be specified as XNXNXN.

France

  • You need to pass STREET, CITY, and ZIPSTATE is not required.
  • ZIP should be specified as NNNNN.

Germany

  • You need to pass STREET, CITY, and ZIPSTATE is not required.
  • ZIP should be specified as NNNNN.

Italy

  • You need to pass STREET, CITY, and ZIPSTATE is optional.  If you do supply a STATE, it should be the two-character province abbreviation (ex.: GE).
  • ZIP should be specified as NNNNN.

Japan

  • You need to pass STREET, CITY, and ZIPSTATE is required only for shipping addresses (if you specify one).  It is not required for billing addresses.
  • The address should be romanized.
  • STATE should be set to the full prefecture name (ex.: Tokyo).
  • CITY should be set to the municipality.
  • STREET should be the location within the municipality (e.g., the rest of the address).
  • ZIP should be specified as NNN-NNNN.
  • For DoDirectPayment calls on the Sandbox, omit the state, as anything else causes an error.  Leave the shipping address off altogether.  (If you’re really adamant about testing it with a state and/or shipping address, set STATE to JP-40.)

As an example, the address of the Tokyo Central Post Office is:

Tokyo Central Post Office
5-3, Yaesu 1-Chome
Chuo-ku, Tokyo 100-8994

In this scenario, I would set my variables accordingly:

  • STREET=”5-3, Yaesu 1-Chome”
  • CITY=”Chuo-ku”
  • STATE=”Tokyo”
  • ZIP=”100-8994″

Spain

  • You need to pass STREET, CITY, and ZIPSTATE is optional.  If you do pass a STATE, it should be the full province name, with accented characters translated into their non-accented equivalents (ex.: Avila).
  • ZIP should be specified as NNNNN.

Sweden

  • You need to pass STREET and CITYSTATE is not required.  ZIP is optional.
  • If you do pass a ZIP, it should be specified as NNNNN.

United Kingdom

  • You only need to pass STREET, CITY, and ZIPSTATE is optional.  If you do pass a STATE, it should be the county name (ex.: West Sussex).
  • Make sure you set COUNTRYCODE to GB, not UK!  (If you look at ISO 3166-1, the United Kingdom specifically reserved it so that no one else would use it, but it’s still not their official country code.)  Passing UK results in a nasty user experience for your buyers.
  • There’s not a good way to describe the format for ZIP, so I’ll just say go look at the Wikipedia article.

United States

  • You need to pass STREET, CITY, STATE, and ZIP.
  • STATE should be the state’s two-character abbreviation (ex.: NY)
  • ZIP should be either the 5 or 9-digit ZIP code, in the format NNNNN, NNNNN-NNNN, NNNNNNNNN, or NNNNN NNNN.

My Experience With CLEAR

Yesterday, a coworker and I went through San Jose-Mineta International Airport to catch a flight home from a business trip.  When we arrived and saw how long the lines were for the security checkpoint, we had a little bit of an “oh shit” moment: we had arrived an hour and a half early — plenty of time to check our bags and get through security — but we thought we were arriving during an off-peak time and didn’t expect the lines to be quite *that* long.  (There were maybe 150-200 people in line, I’m guessing.)  I didn’t have any bags to check, so I told my coworker, “yeah, I’m going to get in line now,” and proceeded to the security checkpoint.

As I got in line, a lady came up to me and offered to sign me up for CLEAR — she said it would only take 3-4 minutes to sign up, I’d get the first month free, and that it would let me bypass the long lines.  That sounded good to me, so I said “sure”.

There was a two-part process: part one was applying for the CLEAR (presumably, something I’d only ever have to do once, or at worst, maybe once a year).  The lady walked me over to a nearby terminal, where she logged in and started the signup process.  They collected the following information from me:

  • Basic info — name, address, email address, phone number
  • Scanned a copy of my driver’s license (both front and back)
  • Credit card (cause the service costs money — but at this point, I didn’t know how much)
  • Fingerprints — from all 10 fingers
  • Photo of my face
  • Social security number — followed by an identity check (you know…where they ask you questions about stuff that’s on your credit report — in my case, “which of these streets have you previously lived on” and “how old is your sister”…which is a little creepy)

After the registration was complete, it was time for part two — actually going through the line.  They walked me over to a separate line, where I bypassed the rest of the people in line for the security checkpoint.  I came up to a couple of terminals, where there were maybe two or three people in front of me.  Once I got to the terminal, I scanned my boarding pass and provided fingerprints from two of my fingers.  The machine quickly said I was all set to go, so they then walked me into a special screening line.  (This part was a little clumsy, because I had to walk past the TSA agents — the ones that check your ID and boarding pass — and I had to go past them in the opposite direction that one would normally go in when going past these people.  Also it was slightly crowded.)  From here, they routed us to a line with an X-ray machine and a walk-through metal detector; we were asked to put our bags on the line to be X-rayed, but they said “everything stays in your bag”.  Do I need to take my laptop out?  Nope.  Do I need to take my baggie of fluids out?  Nope.  Do I need to take my shoes off?  Nope.  Do I need to take my belt off?  Nope.  Do I need to empty my pockets?  Nope — just go through the metal detector.  On the first trip through, I set off the metal detector, so they had me empty my pockets (big whoop — two cell phones and a wallet), put them on the X-ray belt, and go back through.  I didn’t set it off that time, so they waved me through.  I picked up my possessions from the X-ray belt and proceeded out of the checkpoint.

As I exited the checkpoint, my thoughts turned to my coworker.  “I wonder how much time I just saved,” I thought to myself.  He had to check a bag, but that hadn’t taken him very long — I remembered seeing him in line as I was walking through the “special” line — so I pulled out my phone and started my stopwatch.  And then I waited.  Finally, I saw him emerge from the checkpoint — and when I pulled out my phone, I saw that my stopwatch had been running for 21 minutes.

So, what’s my impression of CLEAR?

  • They ask for an awful lot of personal information to make this process work.  On top of that, the person that recruited me didn’t exactly explain how this information was going to be used (past “the credit card is used to pay for the service”).  I suppose it’s understandable, given that it’s airport security, although it’s sad that we have to surrender all but our DNA samples to the government in order to get on a plane nowadays.
  • It was nice not having to unpack half of my bags, take off half of my clothes, or go through the Backscatter X-Ray Scanner of Certain Doom 5000.  (The “5000” makes it sound cooler than its predecessor, the Backscatter X-Ray Scanner of Certain Doom 666.  Travelers — especially the more evangelical ones — didn’t respond well to that, a fact that *somehow* failed to come out during consumer testing.)  I did feel like CLEAR improved that part of the process and made me feel more like a real human being.
  • The price of this service was a bit hefty.  (Notice how the girl that recruited me didn’t tell me how much it was?  There was probably a reason for that.)  As I was walking through the airport to my gate, I got an email telling me how much the service was going to be if I didn’t cancel in the next 28 days — $179.  (Turns out, that’s a per-year charge.)  Unfortunately, this is the second time I’ve traveled this year, so the cost/benefit ratio here doesn’t work in their favor.  When I went to cancel (more on that below), they offered me a discounted rate of $109 per year; however, even that is considerably more expensive than TSA Pre✓ (which is $85 for 5 years, as of this writing — which works out to $17 per year), and doesn’t really offer much of an advantage over TSA Pre✓.
  • CLEAR isn’t available everywhere — in fact, it’s available in very few airports right now (13, as of the time of this writing, with Seattle listed as “Coming Soon”).  Their website has a map showing where they’re available, and even includes a draggable pin that you can drag to the airport where you want them to be available.  However, you can only drag the pin to locations that they’ve pre-defined, and my home airport isn’t one of those choices — which tells me that they’re not going to be available where I am anytime soon.  Having them available in my home airport would make their service doubly useful, as I would be able to use them on both my outgoing flight and my return flight (assuming I’m going through an airport where they’re set up).
  • When I went to their website to cancel, the process was a little clunky.
    • At first, I started up a chat window with them, which sat there and did nothing for at least 20 minutes.  I chalked it up to issues with my company firewall, so I closed it out and didn’t think much of it.
    • Later in the day, I got an email from them that had a “Manage my account” link in it, so I clicked it and tried to log in.  I didn’t know what my password was (the application process didn’t ask me for one), so I used their password feature, which went fairly smoothly.  Once I was logged in, I got an error page (haha, now I know you guys are using force.com!) that wouldn’t go away, even if I logged out and logged back in.
    • Finally, I resorted to trying the web chat again.  This time, it worked and I got a hold of “Tanyia M” immediately.  “She” was very helpful and got my subscription canceled; however, her responses came back to me so quickly that I’m not sure of Tanyia is an actual person or one of our impending bot overlords.

Overall, I liked the experience, but I think this service would be more worth it if I were a frequent traveler with money to burn — or if the service had a much better price point.  As it is, my company is allowing me to travel less and less, so this just doesn’t make it worth it to me.  I don’t think the time savings, when compared to TSA Pre✓, would have been that significant — especially given that I usually show up to the airport far earlier than I need to.  In fact, I just learned that TSA Pre✓ is $85 for 5 years (I thought it was $79 for 1 year), so that option just became a lot more tempting.

Persisting the Volume on the Polycom VVX 500 with a USB Headset

I’m sharing this because this took me forever to figure out, and I’m hoping that this bit of information does someone some good.

I have a Polycom VVX 500 VoIP phone.  This phone is USB enabled, and it so far it’s recognized every USB headset I’ve plugged into it (which has probably been two); however, when I plugged in my headset, the volume would reset to the median setting after every call.  It wouldn’t do that when I connected it through the headset port, however, so I just plugged it in via the headset port and left well enough alone.

That changed today, as I picked up a Plantronics Voyager Legend headset.  The headset only came with a small USB dongle — no way to wire it into the headset jack on the phone.  That’s fine — it’s working great so far, I love the way it fits, I love how well it works with the phone, and I love that it came with its own carrying case — that doubles as a battery-powered charger!  But, my old problem resurfaced — each time I make a call, the volume resets to the middle setting.

After scouring around the web today, I finally found a helpful answer on the Polycom forums, which also describes why this setting even exists in the first place (apparently, some countries have laws that require the phone’s volume to reset to its default setting after each call).  Here’s how to make it work:

  1. You’ll need to enable the web interface, if you haven’t done so already.  To do this, go into the settings app and go to Advanced (the default admin password is 456)->Administration Settings->Web Server Configuration->Web Server and set it to Enabled.  Set Web Config Mode to something other than Disabled (I suggest setting it to HTTP Only.)  Exit out of the menu.  (The phone will probably reboot at this point.  Wait for it to come back up before proceeding to the next step.)
  2. On your computer, pull up a web browser and type in the phone’s IP address.  (You can find this in the settings app under Status->Network->TCP/IP Parameters.)
  3. Log in as the admin user (again, the default password is 456).
  4. Go to Utilities->Import & Export Configuration.
  5. In your favorite text editor, create a file with the following contents (yes, it’s just one line):
    <Volume voice.volume.persist.usbHeadset="1"/>
  6. Under Import Configuration, click Choose File.  Choose the file you created in the previous step, and click Import.

Now you should be done!  The phone should remember your volume preferences between calls now.

Distributed Unwealth

The latest Powerball jackpot seems to be making everyone a little crazy and/or stupid (myself included), but sometimes you just gotta call people out on it when they take the “stupid” part too far.

Today, one of my Facebook friends shared a very stupid picture.  I’m not calling the friend who shared the picture stupid; I’m calling the picture itself stupid, for multiple reasons.

Some Facebook Meme

There are several issues with this:

  • There’s a very simple math error; $1.3 billion divided by 300 million people is actually $4.33, not $4.33 million.  (EDIT: It occured to me that the person who made this might have used the archaic British English definition of “billion”, which was one million million, or one trillion in American English.  However, even if we use this definition — which isn’t common today — we end up with $4,333.33, not $4.33 million.)
  • $1.3 billion is an annuity payout.  It’s paid out in 30 equal installments over the course of the next 29 years.  Spread out over 300 million people, that actually comes out to about $0.14 per person, per year.
  • This fails to account for taxes that would be withheld.

The second point actually makes a good case for taking the lump sum option — as of the time of this writing, the cash payout was estimated at $806 million, which comes out to about $2.69 per person — still not $4.33 million (or even $4.33), but now we’re starting to get a little more realistic.

Even at $2.69, we’re still not accounting for tax withholdings.  I don’t have the patience to go and figure out how much would be taken out of a Powerball jackpot for taxes; fortunately, other people have done that work for me.  It’s going to depend on what state you live in — you’re going to be much better off if you live in California, Delaware, Florida, New Hampshire, Pennsylvania, Puerto Rico, South Dakota, Tennessee, Texas, Washington, or Wyoming — but for sake of argument, we’ll take my home state, which seems to have a 5% tax on lottery winnings.  If I took the lump sum option here, I’d get $564.2 million after taxes, which works out to $1.88 per person.

Even this number is slightly off, because 300 million people is a pretty bad rounding of the US population; according to the US Census Bureau’s estimates, the current population is closer to 322.8 million people.  (Where did those other 22.8 million people go, random internet person??  It’s like the Holocaust all over again…)  Using this figure, we get closer to the neighborhood of $1.75 per person.

Now, let’s face reality: this would never happen.  In a best-case scenario, you would take your winning ticket into your state’s lottery office and tell them “I want this jackpot paid out to everyone in the United States.”  There is precedent for splitting jackpots among multiple winners (and my state’s lottery office confirmed that there’s no upper limit on the number of people that can split a jackpot), but the logistical nightmare caused by splitting it among 322 million people would most likely be enough for them to tell you “no” on the spot.  In a next-best-case scenario, you would turn the money over to the federal government and ask them to do the same; but without an act of Congress, they’re unlikely to oblige.  This means you’re left to do this on your own, and the likelihood of tracking down the home address of every single person in the United States is next to nil, even in today’s world.  And, even if you did, you’re going to have to add in administrative costs for paying out to every single person — you’re likely going to pay almost $84 million alone just in postage to mail a check out to everyone.

Lastly, we have to ask the question — what good would it do to even mail $4.33 to everyone in the US?  $4.33 doesn’t go far in today’s America.  For most people, this might be a couple gallons of gas or a cheap meal at McDonald’s.  The benefits of spreading the wealth around in this manner just aren’t there.  (This is not to say that you couldn’t take your Powerball winnings and put them towards a good cause — but there are better ways to do it than by splitting it up and sending everyone a check.)

Now then — this post actually provoked a rather interesting thought.  What if you took the annuity option?  Would you split it up between everyone who was alive at the time you got the first payment?  Or would you take each annual payment and split it between everyone who was alive in the US at the time?  For the sake of this argument, let’s ignore administrative costs — let’s assume everyone signs up for direct deposit and it costs us a pittance to actually do so (split up, of course, over 30 equal installments of 1/30th of a pittance).  Let’s also assume that we don’t have the Fed’s cooperation here — we’re going to be stuck paying taxes on the initial amount, and we’ll divvy up the rest to everyone after taxes are paid.

The first option actually works out better for the people who are alive at the time of the first payout, because the truth is that not all of them will live to see all 30 payouts (or even the second payout).  The US Census Bureau has the death rate (as of 2015) pegged at 8.2 per 1,000 people per year; this means that, of our 322.8 million people, only about 320.2 million of them will live to see 2017’s payout.  After taxes in my home state, each year’s payout is going to be $30⅓ million.  This means that the first year’s payout is going to be a whopping $0.094 per person; the second year’s payout will fall just shy of $0.095 per person.  The good news is that the death rate is projected to increase over time: by 2050 (just a few years after our experiment is over), it’ll top out at 10.3 per thousand people per year.  By the time of the last payout, 75 million of you will be dead; those who are left will get about $0.122 each.  Here’s how the math works out:

Year Starting Population Payout (Per Person) Death Rate (Per 1,000) Ending Population
2016  322,814,965  $0.094 8.22  320,161,426
2017  320,161,426  $0.095 8.24  317,523,296
2018  317,523,296  $0.096 8.26  314,900,554
2019  314,900,554  $0.096 8.28  312,293,177
2020  312,293,177  $0.097 8.3  309,701,144
2021  309,701,144  $0.098 8.37  307,108,945
2022  307,108,945  $0.099 8.44  304,516,946
2023  304,516,946  $0.100 8.51  301,925,507
2024  301,925,507  $0.100 8.58  299,334,986
2025  299,334,986  $0.101 8.65  296,745,738
2026  296,745,738  $0.102 8.72  294,158,115
2027  294,158,115  $0.103 8.79  291,572,465
2028  291,572,465  $0.104 8.86  288,989,133
2029  288,989,133  $0.105 8.93  286,408,460
2030  286,408,460  $0.106 9  283,830,784
2031  283,830,784  $0.107 9.1  281,247,924
2032  281,247,924  $0.108 9.2  278,660,443
2033  278,660,443  $0.109 9.3  276,068,901
2034  276,068,901  $0.110 9.4  273,473,853
2035  273,473,853  $0.111 9.5  270,875,851
2036  270,875,851  $0.112 9.6  268,275,443
2037  268,275,443  $0.113 9.7  265,673,171
2038  265,673,171  $0.114 9.8  263,069,574
2039  263,069,574  $0.115 9.9  260,465,185
2040  260,465,185  $0.116 10  257,860,533
2041  257,860,533  $0.118 10.03  255,274,192
2042  255,274,192  $0.119 10.06  252,706,134
2043  252,706,134  $0.120 10.09  250,156,329
2044  250,156,329  $0.121 10.12  247,624,747
2045  247,624,747  $0.122 10.15  245,111,356

Now, keep in mind — my math isn’t perfect.  The Census’s death rates are only calculated at 10-year intervals starting in 2020 (except that they also did one for 2015), and I just did a simple linear interpolation of the death rate for the years in between.  If someone else can come up with a better model, please send it my way.

Figuring out the second option is easier, because the US Census Bureau has already done most of the work for me.  They’ve put together projections showing what the population of the US will be each year, through 2060, so I don’t have to bother with birth rates/death rates and trying to figure out what the population is going to be each year.  If we’re just going to take each year’s annuity and split it between everyone that’s alive in the US at that time, the first year’s payout will be the biggest — $0.094.  The second year’s payout will still be around $0.094, but it’ll slowly decline, and by year 29, you’ll only be getting $0.078:

Year Starting Population Payout (Per Person)
2016  323,996,000  $0.0936
2017  326,626,000  $0.0929
2018  329,256,000  $0.0921
2019  331,884,000  $0.0914
2020  334,503,000  $0.0907
2021  337,109,000  $0.0900
2022  339,698,000  $0.0893
2023  342,267,000  $0.0886
2024  344,814,000  $0.0880
2025  347,335,000  $0.0873
2026  349,826,000  $0.0867
2027  352,281,000  $0.0861
2028  354,698,000  $0.0855
2029  357,073,000  $0.0849
2030  359,402,000  $0.0844
2031  361,685,000  $0.0839
2032  363,920,000  $0.0834
2033  366,106,000  $0.0829
2034  368,246,000  $0.0824
2035  370,338,000  $0.0819
2036  372,390,000  $0.0815
2037  374,401,000  $0.0810
2038  376,375,000  $0.0806
2039  378,313,000  $0.0802
2040  380,219,000  $0.0798
2041  382,096,000  $0.0794
2042  383,949,000  $0.0790
2043  385,779,000  $0.0786
2044  387,593,000  $0.0783
2045  389,394,000  $0.0779
Figure 2 -- Powerball Life Chart
Figure 2 — Powerball Life Chart

Conclusion: This idea of spreading out the Powerball winnings between everyone in the US is stupid.  Just give me your Powerball tickets instead.

Update 1: On a whim, I called up my state’s lottery office; they confirmed that there’s no upper limit to the number of people that can split a jackpot.

My Stance on Gun Control

A few days ago, I made a comment on Facebook.  There was a part of me — probably the same part that reminds me how to walk and talk — that expected that this comment would generate some controversy…and it did.  For the record, here is what I said:

Before we all cry out and say “we need stricter gun control laws”, just remember — criminals don’t care what the laws are. There will always be criminals, and they’ll always find a way to get a hold of a gun, regardless of what restrictions you put in place. We keep trying the wrong approach — one of these days, I hope that we figure out that gun control laws aren’t the answer.

This comment was primarily a knee-jerk reaction to certain events that took place that day; and as I kind of expected, a number of people replied to this post (some through other means than Facebook)…some expressing support, and some critical.  I don’t think I’m wrong for what I said, but I didn’t have the time right then to clarify my position further.  So, I’ll take this opportunity to explain my position.

By all accounts, I should be a liberal.  I’m pro-choice.  I support gay marriage.  I’m in favor of legalizing marijuana.  I believe in leaner government, lower taxes, and big business.  And, I drive a Subaru — something which revealed to my extended family that I voted for Obama in the last election and subsequently got me kicked out of the house after my grandmother’s funeral.

However, I’m also something of an enigma, because I’m also in favor of gun rights.  I believe that you should have the right to defend yourself, your home, and your family from those who would wish you harm.  (At the same time, guns scare me and I can’t see myself ever owning one.)  But we seem to have a problem with gun violence in this country, and it seems that people’s natural reaction to a horrible event involving guns is to blame the guns.  I don’t buy this.  There’s an old adage that says “Guns don’t kill people.  I kill people.”  I believe that — the problem is not with guns, the problem is with people.

The cry I hear so often is that we need to do more to stop mass shootings.  “There’s no reason people need to own high-capacity automatic weapons, because their only purpose is to inflict mass casualties,” I hear people say.  Mass shootings are a horrible thing, don’t get me wrong, and on-air executions doubly so, but they are a tiny piece of the problem.  We think they’re a huge problem because they’re a public spectacle, but in reality, they account for less than 1% of all gun-related homicides.  When we put measures in place to try to prevent mass shootings or reduce the amount of damage they can do — like banning automatic weapons and high-capacity magazines — we’re not addressing the real problem.  If we’re going to solve the gun problem in this country, we have to look at how to prevent the other 99%.

The commonly held belief is “less guns, less violence” (and, by extension, “more guns, more violence”).  But there’s a wealth of evidence to the contrary.  Let’s take a look at England and Wales as an example.  After World War I and in the decades since, England and Wales passed a succession of laws restricting the ownership of guns, culminating in the Firearms Act of 1997, which banned almost all handguns.  Did their homicide rate go down as a result?  Sadly, no.  The immediate result of the gun ban was that homicide rates spiked.  It became so bad that by 2000, England and Wales had a violent crime rate higher than any other European nation, and even higher than the United States.  As noted by Professor Joyce Lee Harper of the George Mason University School of Law, gun violence was not a problem until restrictions on their ownership were instituted:

Armed crime, never a problem in England, has now become one. Handguns are banned but the Kingdom has millions of illegal firearms.  Criminals have no trouble finding them and exhibit a new willingness to use them.  In the decade after 1957, the use of guns in serious crime increased a hundredfold. 1

It should be noted that this is not a new conclusion.  Colin Greenwood, a senior English police official, wrote this as part of his thesis for the Cambridge University Institute of Criminology in the early 1970s:

Half a century of strict controls . . . has ended, perversely, with a far greater use of [handguns] in crime than ever before.

England’s homicide rate didn’t start dropping until around 2003-2004, when they massively increased the size of their police force.  It didn’t return to pre-1997 levels until 2010.  This problem is not unique to England — Ireland, Jamaica, Luxembourg, and Russia have all experienced increases in their homicide rates since their respective gun bans went into effect.

During the same period when England was increasing restrictions on gun ownership, many states in the US were relaxing theirs.  During the 1980s and 1990s, half of all US states passed concealed carry laws, allowing responsible citizens to obtain permits to carry concealed weapons in public.  Today, the homicide rate is less than half of what it was in 1980.  This is no coincidence — multiple studies have been able to show a correlation between the increasing prevalence of concealed carry laws and the reduction in the homicide rate in localities where these laws have been passed. 2

The question must be asked, then, why the US has a homicide rate that is so much higher than other countries where strict gun control laws are in place.  For example, the homicide rate in this country is 5.10 per 100,000 people per year (as of 2013), with 3.55 per 100,000 of those being gun homicides.  By contrast, the homicide rate in the United Kingdom is 1.03 per 100,000 people per year (as of 2011/2012), with only 0.06 being gun homicides.  Is it because England has stricter gun controls than the US does?  Not necessarily.  Many European countries had homicide rates that were at all-time lows before gun controls were introduced.  As Professor Malcolm noted:

The peacefulness England used to enjoy was not the result of strict gun laws.  When it had no firearms restrictions [nineteenth and early twentieth century] England had little violent crime, while the present extraordinarily stringent gun controls have not stopped the increase in violence or even the increase in armed violence.

(At the same time, it should be noted that the homicide rate in the US is well below the global average; there are many other countries where you’re far more likely to be murdered than you are in the US.  And, anecdotally, you’re almost twice as likely in this country to be using a gun to kill yourself than you are to be using it to kill someone else.)

My opinion is that if you pass gun control laws, you simply make it harder to defend yourself against someone that intends to do you harm.  You might make it harder for the criminal to obtain a gun, but I’ve long held the belief that criminals don’t care about laws — if they want something badly enough, they’ll find a way around it, ignore it completely, or find another way to accomplish their means.  In that regard, you actually make the law-abiding citizen a more tempting target, since you reduce the possibility that the law-abiding citizen will be able to defend himself.  As noted in a 2007 study published in the Harvard Journal of Law and Public Policy, having a gun is actually a defective deterrent to committing a crime:

National Institute of Justice surveys among prison inmates find that large percentages report that their fear that a victim might be armed deterred them from confrontation crimes.  “[T]he felons most frightened ‘about confronting an armed victim’ were those from states with the greatest relative number of privately owned firearms.” Conversely, robbery is highest in states that most restrict gun ownership.

It’s not that I don’t think guns are dangerous — they absolutely are — but they’re a tool, just like a hatchet, a band saw, or a power drill.  And like any tool, they can be misused.  If your child attacks someone with a power drill, you blame the child, not the power drill.  Why are guns so different?

So what’s the answer?  I don’t know, but more and more evidence is pointing to the conclusion that gun control laws don’t have an effect on violent crime rates:

In 2004, the U.S. National Academy of Sciences released its evaluation from a review of 253 journal articles, 99 books, 43 government publications, and some original empirical research.  It failed to identify any gun control that had reduced violent crime, suicide, or gun accidents.  The same conclusion was reached in 2003 by the U.S. Centers for Disease Control’s review of then‐extant studies.

I’m in favor of gun rights because I don’t think guns are the problem — people are the problem.  We need to have a conversation in this country about anger, and find ways to address that problem before it escalates to murder.  We need to address the reasons why people kill, not the tools that people use to do it.  Humans are smart and resourceful, and if you take away the tools they need to carry out a task, they will simply find new tools.


  1. See Would Banning Firearms Reduce Murder and Suicide?, page 655.
  2. See Would Banning Firearms Reduce Murder and Suicide?, page 658, footnote 30.

Retrieving Your PayPal API Credentials

I originally posted this article to x.com on August 10, 2010. Since that time, x.com has been repurposed, and my posts have been taken down. I have reposted this here for informational and historical purposes.

Update 9/16/2014: I’ve tried to update this information to reflect the myriad of different account layouts that are available as of today.

One of the very first things that you must do when implementing Website Payments Pro, Express Checkout, Mobile Checkout, Adaptive Payments, or Adaptive Accounts is to get your API credentials.  For someone like me, who has done this a hundred times, this is pretty simple; but I get calls almost every day from new merchants and developers who just don’t know what API credentials are, and where to go to get them.  Further, in all my browsing through paypal.com and x.com, I don’t think I’ve ever seen a clear, concise guide on how to retrieve your API credentials.  So, here’s the answer.

Continue reading “Retrieving Your PayPal API Credentials”

Sign Your Card, Damnit

I’ve worked in a retail store that has been prone to credit card fraud.  My wife used to work in credit card disputes.  My close friend works fraud.  And in all my years, there’s one thing I never understood — why people don’t sign the back of their credit cards.  I can be a stickler for rules, and there was a point in time where I tried to make people sign their cards before I would accept them — even if they wrote “See ID” on the back of the card — but I gave up on that because it ended up pissing off more people than I wanted to deal with.  Still, I never understood why people didn’t sign the back of their cards.  (If you’re one of those people that writes “See ID” on the back of your card, this post is for you.)

Continue reading “Sign Your Card, Damnit”