├── .gitignore ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 GoCardless 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP API Design Standards 2 | 3 | ## Guidelines 4 | 5 | This document provides guidelines and examples for GoCardless APIs, encouraging consistency, maintainability, and best practices. 6 | 7 | ### Sources: 8 | 9 | - https://github.com/interagent/http-api-design 10 | - https://www.gov.uk/service-manual/making-software/apis.html 11 | - http://www.mnot.net/blog/ 12 | - http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api 13 | - https://github.com/WhiteHouse/api-standards 14 | - http://apigee.com/about/content/api-fa%C3%A7ade-pattern 15 | - https://pages.apigee.com/web-api-design-ebook.html 16 | - https://groups.google.com/forum/#!forum/api-craft 17 | 18 | ## JSON API 19 | 20 | All endpoints must follow the core [JSON API spec](http://jsonapi.org/format/) 21 | 22 | Changes from JSON API: 23 | 24 | - The primary resource must be keyed by its resource type. The endpoint URL must also match the resource type. 25 | - API errors do not currently follow the JSON API spec. 26 | - Updates should always return `200 OK` with the full resource to simplify internal logic. 27 | 28 | Example of keying the resource by type: 29 | 30 | ```json 31 | GET /posts/1 32 | ``` 33 | 34 | ```json 35 | { 36 | "posts": { 37 | "id": 1 38 | } 39 | } 40 | ``` 41 | 42 | All action calls to an endpoint must be wrapped in a `data` envelope. 43 | This is because actions are carried out on a resource but use a different data type than the core resource itself. 44 | 45 | Example of a action call: 46 | 47 | ```json 48 | POST /posts/action/share 49 | 50 | { 51 | "data": { 52 | "recipientID": "U123" 53 | } 54 | } 55 | ``` 56 | 57 | ## JSON only 58 | 59 | The API should only support JSON. 60 | 61 | Reference: http://www.mnot.net/blog/2012/04/13/json_or_xml_just_decide 62 | 63 | ## General guidelines 64 | 65 | - A URL identifies a resource. 66 | - URLs should include nouns, not verbs. 67 | - For consistency, only use plural nouns (e.g. "posts" instead of "post"). 68 | - Use HTTP verbs (`GET`, `POST`, `PUT`, `DELETE`) to operate on resources. 69 | - Use filtering instead of nested resources: `/payments?subscription=xyz` rather than 70 | `/subscriptions/xyz/payments`. Nested resources enforce relationships that could 71 | change and makes clients harder to write. 72 | - API versions should be represented as dates documented in a changelog. Version number should not be in the URL. 73 | - API should be behind a subdomain: `api.gocardless.com` 74 | 75 | ## RESTful URLs 76 | 77 | ### Good URL examples 78 | 79 | - List of payments: 80 | - `GET https://api.gocardless.com/payments` 81 | - Filtering is a query: 82 | - `GET https://api.gocardless.com/payments?status=failed&sort_field=-created` 83 | - `GET https://api.gocardless.com/payments?sort_field=created` 84 | - A single payment: 85 | - `GET https://api.gocardless.com/payments/1234` 86 | - All amendments in (or belonging to) this subscription: 87 | - `GET https://api.gocardless.com/subscription_amendments?subscription=1234` 88 | - Include nested resources in a comma separated list: 89 | - `GET https://api.gocardless.com/payments/1234?include=events` 90 | - Include only selected fields in a comma separated list: 91 | - `GET https://api.gocardless.com/payments/1234?fields=amount` 92 | - Get multiple resources: 93 | - `GET https://api.gocardless.com/payments/1234,444,555,666` 94 | - Action on resource: 95 | - `POST https://api.gocardless.com/payments/1234/actions/cancel` 96 | 97 | ### Bad URL examples 98 | 99 | - Singular nouns: 100 | - `GET https://api.gocardless.com/payment` 101 | - `GET https://api.gocardless.com/payment/123` 102 | - `GET https://api.gocardless.com/payment/action` 103 | - Verbs in the URL: 104 | - `GET https://api.gocardless.com/payment/create` 105 | - Nested resources: 106 | - `GET https://api.gocardless.com/subscriptions/1234/amendments` 107 | - Filtering outside of query string: 108 | - `GET https://api.gocardless.com/payments/desc` 109 | - Filtering to get multiple resources: 110 | - `GET https://api.gocardless.com/payments?id[]=11&id[]=22` 111 | 112 | ## HTTP verbs 113 | 114 | Here's an example of how HTTP verbs map to create, read, update, delete operations in a particular context: 115 | 116 | | HTTP METHOD | POST | GET | PUT | PATCH | DELETE | 117 | | :---------- | :-------------- | :------------------ | :------------------------------------------------- | :------------------------------------------------------------ | :--------------- | 118 | | CRUD OP | CREATE | READ | UPDATE | UPDATE | DELETE | 119 | | /plans | Create new plan | List plans | Bulk update | Error | Delete all plans | 120 | | /plans/1234 | Error | Show Plan If exists | If exists, full/partial update Plan; If not, error | If exists, update Plan using JSON Patch format; If not, error | Delete Plan | 121 | 122 | ## Actions 123 | 124 | Avoid resource actions. Create separate resources where possible. 125 | 126 | #### Good 127 | 128 | ```http 129 | POST /refunds?payment=ID&amount=1000 130 | ``` 131 | 132 | #### Bad 133 | 134 | ```http 135 | POST /payments/ID/refund 136 | ``` 137 | 138 | Where special actions are required, place them under an `actions` prefix. 139 | Actions should always be idempotent. 140 | 141 | ```http 142 | POST /payments/ID/actions/cancel 143 | ``` 144 | 145 | ## Responses 146 | 147 | Don’t set values in keys. 148 | 149 | #### Good 150 | 151 | ```json 152 | "tags": [ 153 | {"id": "125", "name": "Environment"}, 154 | {"id": "834", "name": "Water Quality"} 155 | ] 156 | ``` 157 | 158 | #### Bad 159 | 160 | ```json 161 | "tags": [ 162 | {"125": "Environment"}, 163 | {"834": "Water Quality"} 164 | ] 165 | ``` 166 | 167 | ## String IDs 168 | 169 | Always return string ids. Some languages, like JavaScript, don't support big ints. Serialize/deserialize ints to strings if storing ids as ints. 170 | 171 | ## Error handling 172 | 173 | Error responses should include a message for the user, an internal error type (corresponding to some 174 | specific internally determined constant represented as a string), and links to info for developers. 175 | 176 | There must only be one top level error. Errors should be returned in turn. This makes internal error 177 | logic and dealing with errors as a consumer of the API easier. 178 | 179 | Validation and resource errors are nested in the top level error under `errors`. 180 | 181 | The error is nested in `error` to make it possible to add, for example, deprecation errors on successful requests. 182 | 183 | The HTTP status `code` is used as a top level error, `type` is used as a sub error, and nested 184 | `errors` may have more specific `type` errors, such as `invalid_field`. 185 | 186 | ### Formating errors vs integration errors 187 | 188 | Formatting errors should be separate from errors handled by the integration. 189 | 190 | Formatting errors include things like field presence and length, and are returned when incorrect 191 | data is sent to the API. 192 | 193 | An example of an error that should be handled by the integration is attempting to create a payment 194 | against a mandate that has expired. This is an edge case that the API integration needs to 195 | handle. Do not mask these errors as validation errors. Return them as a top level error instead. 196 | 197 | ### Top level error 198 | 199 | - Top level errors MUST implement `request_id`, `type`, `code` and `message`. 200 | - `type` MUST relate to the `reason`. For example, use it to categorise the error: `reason: api_error`. 201 | - `message` MUST be specific. 202 | - Top level errors MAY implement `documentation_url`, `request_url` and `id`. 203 | - Only return `id` for server errors (5xx). The `id` should point to the exception you track internally. 204 | 205 | ```json 206 | { 207 | "error": { 208 | "documentation_url": "https://api.gocardless.com/docs/beta/errors#access_forbidden", 209 | "request_url": "https://api.gocardless.com/requests/REQUEST_ID", 210 | "request_id": "REQUEST_ID", 211 | "id": "ERROR_ID", 212 | "type": "access_forbidden", 213 | "code": 403, 214 | "message": "You don't have the right permissions to access this resource" 215 | } 216 | } 217 | ``` 218 | 219 | ### Nested errors 220 | 221 | - Nested errors MUST implement `reason` and `message`. 222 | - `reason` MUST be specific to the error. 223 | - Nested errors MAY implement `field`. 224 | 225 | ```json 226 | { 227 | "error": { 228 | "top level errors": "...", 229 | 230 | "errors": [ 231 | { 232 | "field": "account_number", 233 | "reason": "missing_field", 234 | "message": "Account number is required" 235 | } 236 | ] 237 | } 238 | } 239 | ``` 240 | 241 | ### HTTP status code summary 242 | 243 | - `200 OK` - everything worked as expected. 244 | - `400 Bad Request` - e.g. invalid JSON. 245 | - `401 Unauthorized` - no valid API key provided. 246 | - `402 Request Failed` - parameters were valid but request failed. 247 | - `403 Forbidden` - missing or invalid permissions. 248 | - `404 Not Found` - the requested item doesn’t exist. 249 | - `422 Unprocessable Entity` - parameters were invalid/validation failed. 250 | - `500, 502, 503, 504 Server errors` - something went wrong on GoCardless’ end. 251 | 252 | #### 400 Bad Request 253 | 254 | - When the request body contains malformed JSON. 255 | - When the JSON is valid but the document structure is invalid (e.g. passing an array when an object should be passed). 256 | 257 | #### 422 Unprocessable Entity 258 | 259 | - When model validations fail for fields (e.g. name too long). 260 | - Trying to create a resource when a related resource is in a bad state. 261 | 262 | ## What changes are considered “backwards-compatible”? 263 | 264 | - Adding new API resources. 265 | - Adding new optional request parameters to existing API methods. 266 | - Adding new properties to existing API responses. 267 | - Changing the order of properties in existing API responses. 268 | - Changing the length or format of object IDs or other opaque strings. 269 | - This includes adding or removing fixed prefixes (such as ch\_ on charge IDs). 270 | - You can safely assume object IDs we generate will never exceed 128 characters, but you should be 271 | able to handle IDs of up to that length. If for example you’re using MySQL, you should store IDs 272 | in a VARCHAR(128) COLLATE utf8_bin column (the COLLATE configuration ensures case-sensitivity in 273 | lookups). 274 | - Adding new event types. Your webhook listener should gracefully handle unfamiliar events types. 275 | 276 | ## Versioning changes 277 | 278 | The versioning scheme is designed to promote incremental improvement to the API and discourage rewrites. 279 | 280 | Server initiated events such as webhooks should not contain serialised resources. If a resource 281 | changed, provide its id instead and let the client request it using a version. 282 | 283 | ### Format 284 | 285 | Versions should be dated as ISO8601 (YYYY-MM-DD) 286 | 287 | - Good: 2014-05-04 288 | - Bad: v-1.1, v1.2, 1.3, v1, v2 289 | 290 | ### Version maintenance 291 | 292 | Maintain old API versions for at least 6 months. 293 | 294 | ### Implementation guidelines 295 | 296 | The API version must be set using a custom HTTP header. The API version must not be defined in the 297 | URL structure (e.g. `/v1`) because it makes incremental change impossible. 298 | 299 | #### HTTP Header 300 | 301 | `GoCardless-Version: 2014-05-04` 302 | 303 | Enforce the header on all requests. 304 | 305 | Validate the version against available versions. Do not allow dates up to a version. 306 | 307 | The API changelog must only contain backwards-incompatible changes. All non-breaking changes are automatically available to old versions. 308 | 309 | Reference: https://stripe.com/docs/upgrades 310 | 311 | ## X-Headers 312 | 313 | The use of `X-Custom-Header` has [been deprecated](http://tools.ietf.org/html/rfc6648). 314 | 315 | ## Resource filtering 316 | 317 | Resource filters MUST be in singular form. 318 | 319 | Multiple ids should be supplied to a filter as a comma separated list, and should be translated into an `OR` query. Chaining multiple filters with `&` should be translated into an `AND` query. 320 | 321 | #### Good 322 | 323 | ```http 324 | GET /refunds?payment=ID1,ID2&customer=ID1 325 | ``` 326 | 327 | #### Bad 328 | 329 | ```http 330 | GET /refunds?payments=ID1,ID2&customer=ID1 331 | ``` 332 | 333 | ## Pagination 334 | 335 | All list/index endpoints must be paginated by default. Pagination must be reverse chronological. 336 | 337 | Only support cursor or time based pagination. 338 | 339 | #### Defaults 340 | 341 | `limit=50` 342 | `after=NEWEST_RESOURCE` 343 | `before=null` 344 | 345 | #### Limits 346 | 347 | `limit=500` 348 | 349 | Parameters: 350 | 351 | | Name | Type | Description | 352 | | -------- | :----: | -----------------: | 353 | | `after` | string | id to start after | 354 | | `before` | string | id to start before | 355 | | `limit` | string | number of records | 356 | 357 | ### Response 358 | 359 | Paginated results are always enveloped: 360 | 361 | ``` 362 | { 363 | "meta": { 364 | "cursors": { 365 | "after": "abcd1234", 366 | "before": "wxyz0987" 367 | }, 368 | "limit": 50 369 | }, 370 | "payments": [{ 371 | … 372 | }, 373 | …] 374 | } 375 | ``` 376 | 377 | ## Updates 378 | 379 | Full or partial updates using `PUT` should replace any parameters passed and ignore fields not submitted. 380 | 381 | ``` 382 | GET /items/id_123 383 | { 384 | "id": "id_123", 385 | "meta": { 386 | "created": "date", 387 | "published": false 388 | } 389 | } 390 | ``` 391 | 392 | ``` 393 | PUT /items/id_123 { "meta": { "published": true } } 394 | { 395 | "id": "id_123", 396 | "meta": { 397 | "published": false 398 | } 399 | } 400 | ``` 401 | 402 | ### PATCH Updates 403 | 404 | PATCH is reserved for [JSON Patch](http://jsonapi.org/format/#patch) operations. 405 | 406 | ## JSON encode POST, PUT & PATCH bodies 407 | 408 | `POST`, `PUT` and `PATCH` expect JSON bodies in the request. `Content-Type` header MUST be set to `application/json`. 409 | For unsupported media types a `415` (Unsupported Media Type) response code is returned. 410 | 411 | ## Caching 412 | 413 | Most responses return an `ETag` header. Many responses also return a `Last-Modified` header. The 414 | values of these headers can be used to make subsequent requests to those resources using the 415 | `If-None-Match` and `If-Modified-Since` headers, respectively. If the resource has not changed, the 416 | server will return a `304 Not Modified`. Note that making a conditional request and receiving a 304 417 | response does _not_ count against your rate limit, so we encourage you to use it whenever possible. 418 | 419 | `Cache-Control: private, max-age=60` 420 | `ETag: ` 421 | `Last-Modified: updated_at` 422 | 423 | #### Vary header 424 | 425 | The following header values must be declared in the Vary header: `Accept`, `Authorization` and `Cookie`. 426 | 427 | Any of these headers can change the representation of the data and should invalidate a cached 428 | version. This can be useful if users have different accounts to do admin, each with different 429 | privileges and resource visibility. 430 | 431 | Reference: https://www.mnot.net/cache_docs/ 432 | 433 | ## Compression 434 | 435 | All responses should support gzip. 436 | 437 | ## Result filtering, sorting & searching 438 | 439 | See JSON-API: http://jsonapi.org/format/#fetching-filtering 440 | 441 | ## Pretty printed responses 442 | 443 | JSON responses should be pretty printed. 444 | 445 | ## Time zone/dates 446 | 447 | Explicitly provide an ISO8601 timestamp with timezone information (DateTime in UTC). 448 | Use the exact timestamp for API calls that allow a timestamp to be specified. 449 | These timestamps look something like `2014-02-27T15:05:06+01:00`. ISO 8601 UTC format: YYYY-MM-DDTHH:MM:SSZ. 450 | 451 | ## HTTP rate limiting 452 | 453 | All endpoints must be rate limited. The current rate limit status is returned in the HTTP headers of 454 | all API requests. 455 | 456 | ```http 457 | Rate-Limit-Limit: 5000 458 | Rate-Limit-Remaining: 4994 459 | Rate-Limit-Reset: Thu, 01 Dec 1994 16:00:00 GMT 460 | Content-Type: application/json; charset=utf-8 461 | Connection: keep-alive 462 | Retry-After: Thu, 01 May 2014 16:00:00 GMT 463 | 464 | RateLimit-Reset uses the HTTP header date format: RFC 1123 (Thu, 01 Dec 1994 16:00:00 GMT) 465 | ``` 466 | 467 | Exceeding rate limit: 468 | 469 | ```http 470 | // 429 Too Many Requests 471 | { 472 | "message": "API rate limit exceeded.", 473 | "type": "rate_limit_exceeded", 474 | "documentation_url": "http://developer.gocardless.com/#rate_limit_exceeded" 475 | } 476 | ``` 477 | 478 | ## CORS 479 | 480 | Support Cross Origin Resource Sharing (CORS) for AJAX requests. 481 | 482 | Resources: 483 | 484 | - [CORS W3C working draft](https://www.w3.org/TR/cors/) 485 | - [HTML5 Rocks](http://www.html5rocks.com/en/tutorials/cors/) 486 | 487 | Any domain that is registered against the requesting account is accepted. 488 | 489 | ```http 490 | $ curl -i https://api.gocardless.com -H "Origin: http://dvla.com" 491 | HTTP/1.1 302 Found 492 | Access-Control-Allow-Origin: * 493 | Access-Control-Expose-Headers: ETag, Link, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, OAuth-Scopes, Accepted-OAuth-Scopes 494 | Access-Control-Allow-Credentials: false 495 | 496 | // CORS Preflight request 497 | // OPTIONS 200 498 | Access-Control-Allow-Origin: * 499 | Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Requested-With 500 | Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE 501 | Access-Control-Expose-Headers: ETag, RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset 502 | Access-Control-Max-Age: 86400 503 | Access-Control-Allow-Credentials: false 504 | ``` 505 | 506 | ## TLS/SSL 507 | 508 | All API request MUST be made over SSL, including outgoing web hooks. Any non-secure requests 509 | return `ssl_required`, and no redirects are performed. 510 | 511 | ```http 512 | HTTP/1.1 403 Forbidden 513 | Content-Length: 35 514 | 515 | { 516 | "message": "API requests must be made over HTTPS", 517 | "type": "ssl_required", 518 | "docs": "https://developer.gocardless.com/errors#ssl_required" 519 | } 520 | ``` 521 | 522 | ## Include related resource representations 523 | 524 | See JSON-API: http://jsonapi.org/format/#fetching-includes 525 | 526 | ## Limit fields in response 527 | 528 | See JSON-API: http://jsonapi.org/format/#fetching-sparse-fieldsets 529 | 530 | ## Unique request identifiers 531 | 532 | Set a `Request-Id` header to aid debugging across services. 533 | --------------------------------------------------------------------------------