🍫 CocoaPatterns — iOS & Mac Development Since 2009 RSS
🍫 CocoaPatterns
Because someone has to write this stuff down
"We ship apps. Sometimes they even work."

API Design by Obscurity: If They Can't Understand It, They Can't Misuse It

The most secure API is one where even authenticated users can't figure out how to call it. A case study inspired by a large social media company.

PM
Priya Menon-Shetty
@priya_api_first · CocoaPatterns

I've spent six years designing APIs. I've written REST APIs. I've written GraphQL APIs. I've written an API that only returns XML because the client was a bank and banks believe XML is alive and well. It is not. But they're a bank and they have money, so: XML.

But the most fascinating API design philosophy I've ever encountered is one I'm calling API Design by Obscurity, or ADBO. I first observed it while integrating with a major social media platform's developer APIs. I will not name the company because (a) I'm still using their APIs and (b) their legal team is larger than our entire engineering department.

"The best documentation is no documentation. The second-best documentation is documentation that contradicts itself. The third-best is documentation from 2016 that references API versions that no longer exist."

Core Principles of ADBO

1. Version Everything, Explain Nothing

The API has versioned endpoints: /v1/, /v2.0/, /v2.1/, /v3.0/, /v3.2/, /v3.3/, /v14.0/, /v18.0/. The version numbers jump seemingly at random. There was no /v4/ through /v13/. Nobody knows what happened. The changelog for v14.0 says "various improvements" and links to a page that 404s.

2. The Response Shape Is a Suggestion

The documentation says a user object returns id, name, and email. The actual response returns id, name, something called name_format (undocumented), and a nested object called picture that contains a data object that contains a url field — but only if you request it via a fields parameter that you learned about from a Stack Overflow answer from 2017.

{
  "id": "10228847293847",
  "name": "Brad Kowalski",
  "name_format": "{first} {last}",
  "picture": {
    "data": {
      "height": 50,
      "is_silhouette": false,
      "url": "https://platform-lookaside.fbsbx.com/...",
      "width": 50
    }
  },
  "updated_time": "2014-03-15T00:00:00+0000"
}

You requested three fields. You received six. Two are undocumented. The updated_time is from 2014 and does not correspond to any known event. This is ADBO working as intended.

3. Permissions Are a Labyrinth

To read a user's email, you need the email permission. Straightforward. To read their birthday, you need user_birthday. Also straightforward. To read their friends list, you need user_friends, which returns friends who also use your app, which is a subset so small it's functionally an empty array. The permission exists. The data does not.

To post on behalf of a user, you need publish_actions. This permission was deprecated in 2018. The documentation still references it. There is no replacement. There is a footnote that says "use the Share Dialog instead." The Share Dialog is a client-side component that cannot be called from a server. If you were doing server-side posting: sorry.

⚠️ True Story

I spent four days implementing an integration with a permission that, it turned out, requires a manual review process that takes 6-8 weeks, during which your app is in a state the dashboard calls "In Review" but which I would call "purgatory." My app has been In Review since February. It is now June. I have moved on emotionally.

4. Rate Limits Are a Mystery

The documentation says you get "200 calls per hour per user." In practice, I've made 847 calls in an hour with no issues and gotten rate-limited after 12 calls in a different hour. The rate limit headers are present but the numbers don't correlate with observable behavior. My working theory is that the rate limiter is also a machine learning model and it's learning that I'm annoying.

5. Error Messages Are Poetry

{
  "error": {
    "message": "(#100) Tried accessing nonexisting field (email) on node type (User)",
    "type": "OAuthException",
    "code": 100,
    "fbtrace_id": "A8dK3jH2LMNOP"
  }
}

The field email exists. It's in the documentation. It's in the Graph API Explorer. But it's a "nonexisting field" for my app because I don't have the email permission, which I do have, but which hasn't been approved yet, because my app is In Review. The error type is OAuthException, which implies an authentication problem, but authentication succeeded. The fbtrace_id is useless to me but presumably means something to someone at the company. I imagine it printed on a ticket in a queue that no one checks.

Why ADBO Works (Against All Reason)

Here's the paradox: the platform has millions of developers building on it. The APIs are the foundation of a massive ecosystem. They work — in the sense that if you invest enough time, read enough Stack Overflow answers, and accept that the documentation is more of a mood board than a reference, you can build functional integrations.

ADBO works because:

  • The community is the documentation. The real API docs are on Stack Overflow, GitHub issues, and a developer forum where the top-voted answer to most questions is "this broke in v8.0 and was never fixed."
  • Obscurity filters for commitment. Only dedicated developers make it through the permission review, rate limit mystery, and documentation archaeology. Those who survive are loyal. They have to be. They've invested too much to switch.
  • Breaking changes create engagement. Every time the API changes without warning, thousands of developers post about it on social media. This is technically engagement.

I'm now designing all my APIs using ADBO principles. My first endpoint returns either a user object or a haiku, depending on the request timestamp. Documentation coming never.

3 Comments — Powered by Disqus
GA
gashby_data · 2 weeks ago
I've worked with the API Priya is describing and the nested JSON keys theory is correct. I found a response field called 'data.story.attachments.media.image.uri' which returns a URL sometimes, a boolean other times, and once returned the number 7.
BK
bradkow_ios · 2 weeks ago
I tried to use the Graph API to fetch my own profile picture and the response included what appears to be a Fourier transform of someone else's profile picture. I'm keeping it.
PM
priya_api_firstAuthor · 13 days ago
To be clear, this post is satire inspired by frustrations many developers share. The company in question has millions of developers using their APIs daily. Their approach works. I just don't know *how* it works.