JSON API response examples displayed in code editor showing structured data format
API Development

50+ JSON API Response Examples: Complete Reference Guide

12 min read
1649 words
Share:

50+ JSON API Response Examples: Complete Reference Guide (2025)

You’re building an API. You know what data you need to return, but you’re stuck staring at a blank file wondering: “What should my JSON response actually look like?”

Should you wrap everything in a data object? Where do error messages go? How do you structure pagination? What about authentication tokens?

I’ve been there. You google “JSON API response format” and find either overly simple examples that don’t match real-world complexity, or academic specifications that are impossible to implement.

This guide is different. These are real-world JSON API response templates I’ve used in production systems, refined through years of building APIs that serve millions of requests. You can copy any example and adapt it to your needs immediately.

Whether you’re building your first REST API or standardizing responses across your organization, you’ll find practical, battle-tested templates here.

How to Use This Guide

Each example includes:

  • Actual JSON you can copy - No pseudocode or placeholders
  • HTTP status code - The correct code for each scenario
  • Real-world context - When to use this response
  • Common variations - Alternative structures you might see
  • Best practices - Lessons learned from production use

Pro tip: Keep this page bookmarked. You’ll reference it constantly during API development.

Before we dive in, let’s cover the fundamentals that apply to all these examples.

JSON API Response Fundamentals

Basic Structure

Every JSON API response should be valid JSON. That means:

  • Use double quotes for strings
  • No trailing commas
  • Proper nesting
  • Valid data types

Use our JSON Formatter to validate your responses before deployment.

HTTP Status Codes Matter

The HTTP status code tells clients how to handle the response:

2xx Success:

  • 200 OK - Request succeeded
  • 201 Created - New resource created
  • 204 No Content - Success, but no response body

4xx Client Errors:

  • 400 Bad Request - Invalid request data
  • 401 Unauthorized - Missing or invalid authentication
  • 403 Forbidden - Authenticated but not authorized
  • 404 Not Found - Resource doesn’t exist
  • 429 Too Many Requests - Rate limit exceeded

5xx Server Errors:

  • 500 Internal Server Error - Something broke on the server
  • 502 Bad Gateway - Upstream service failed
  • 503 Service Unavailable - Temporary outage

Content-Type Header

Always set the correct Content-Type header:

Content-Type: application/json; charset=utf-8

This tells clients to parse the response as JSON.

Response Consistency

Pick a structure and stick with it across your entire API. Inconsistent responses confuse developers and break client code.

Now, let’s look at the examples.

Success Responses

200 OK: Get Single Resource

Use case: Fetching a specific user, product, or any single resource by ID.

HTTP Status: 200 OK

{
  "id": 12345,
  "username": "sarah_chen",
  "email": "sarah@example.com",
  "firstName": "Sarah",
  "lastName": "Chen",
  "role": "admin",
  "verified": true,
  "createdAt": "2024-01-15T10:30:00Z",
  "updatedAt": "2025-01-20T14:22:15Z",
  "profile": {
    "bio": "Software engineer passionate about APIs",
    "website": "https://sarahchen.dev",
    "location": "San Francisco, CA",
    "avatar": "https://cdn.example.com/avatars/12345.jpg"
  },
  "settings": {
    "emailNotifications": true,
    "twoFactorEnabled": true,
    "language": "en",
    "timezone": "America/Los_Angeles"
  }
}

Key points:

  • Flat structure for simple attributes
  • Nested objects for logical grouping
  • ISO 8601 timestamps
  • Booleans for flags
  • URLs as strings

Alternative: Wrapped in data object

Some APIs wrap responses in a data key:

{
  "data": {
    "id": 12345,
    "username": "sarah_chen",
    "email": "sarah@example.com"
  }
}

My recommendation: Skip the wrapper for single resources. It adds unnecessary nesting without benefits. Use it only if you need metadata alongside the resource.

200 OK: Get List of Resources

Use case: Fetching multiple users, products, or any collection.

HTTP Status: 200 OK

{
  "data": [
    {
      "id": 1001,
      "name": "Wireless Headphones",
      "price": 79.99,
      "currency": "USD",
      "inStock": true,
      "category": "Electronics"
    },
    {
      "id": 1002,
      "name": "USB-C Cable",
      "price": 12.99,
      "currency": "USD",
      "inStock": true,
      "category": "Accessories"
    },
    {
      "id": 1003,
      "name": "Laptop Stand",
      "price": 45.00,
      "currency": "USD",
      "inStock": false,
      "category": "Accessories"
    }
  ],
  "meta": {
    "total": 3,
    "count": 3
  }
}

Key points:

  • Array wrapped in data key
  • Metadata in separate meta object
  • Consistent object structure within array
  • Clear indication of stock status

Why wrap arrays in data? It allows you to include metadata without mixing it with the array items. This becomes essential for pagination.

201 Created: Resource Created Successfully

Use case: After creating a new user, product, order, etc.

HTTP Status: 201 Created

Headers:

Location: https://api.example.com/v1/users/12346
{
  "id": 12346,
  "username": "new_user",
  "email": "newuser@example.com",
  "firstName": "John",
  "lastName": "Doe",
  "role": "user",
  "verified": false,
  "createdAt": "2025-01-27T18:30:00Z",
  "verificationToken": "verify_abc123xyz789",
  "message": "Account created successfully. Please check your email to verify your account."
}

Key points:

  • Return the created resource with its ID
  • Include server-generated fields (timestamps, tokens)
  • Set Location header to the new resource URL
  • Optional: Include a human-readable message

Real-world example from Stripe:

{
  "id": "cus_NffrFeUfNV2Hib",
  "object": "customer",
  "email": "customer@example.com",
  "created": 1680893839,
  "livemode": false
}

Stripe includes an object field to indicate the resource type. This is useful for APIs that return multiple types of objects.

204 No Content: Success Without Response Body

Use case: Deleting a resource, updating without needing to return data.

HTTP Status: 204 No Content

Response body: (empty)

When you send a 204 response, the body must be completely empty. Don’t return any JSON, not even {}.

Example request:

DELETE /api/v1/users/12345

Example response:

HTTP/1.1 204 No Content
Content-Length: 0

When to use 204 vs 200:

  • 204: Delete operations, updates where client doesn’t need the result
  • 200: Updates where client needs the updated resource

Alternative: 200 with confirmation message

{
  "message": "User deleted successfully",
  "deletedAt": "2025-01-27T18:45:00Z"
}

Some developers prefer this because it provides explicit confirmation. Both approaches are valid.

Error Responses

Error responses are where most APIs fall apart. Inconsistent error formats frustrate developers and make debugging impossible.

Standard Error Response Structure

Here’s a consistent error format that works for all error types:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request data",
    "details": [
      {
        "field": "email",
        "message": "Email address is not valid",
        "value": "notanemail"
      },
      {
        "field": "age",
        "message": "Age must be at least 13",
        "value": 10
      }
    ],
    "timestamp": "2025-01-27T18:50:00Z",
    "path": "/api/v1/users",
    "requestId": "req_abc123xyz789"
  }
}

Key components:

  • code: Machine-readable error code
  • message: Human-readable description
  • details: Array of specific issues (for validation errors)
  • timestamp: When the error occurred
  • path: API endpoint that failed
  • requestId: Unique identifier for debugging

Now let’s see this structure in action for different error types.

400 Bad Request: Validation Errors

Use case: Client sent invalid data (missing required fields, wrong format, etc.)

HTTP Status: 400 Bad Request

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Email address is required",
        "type": "required"
      },
      {
        "field": "password",
        "message": "Password must be at least 8 characters",
        "type": "minLength",
        "constraint": 8
      },
      {
        "field": "age",
        "message": "Age must be a number",
        "type": "type",
        "expected": "number",
        "received": "string"
      }
    ],
    "timestamp": "2025-01-27T19:00:00Z",
    "path": "/api/v1/users",
    "requestId": "req_validation_001"
  }
}

Best practices:

  • List ALL validation errors, not just the first one
  • Include the field name for programmatic handling
  • Provide constraint values (min, max, pattern)
  • Use consistent error codes

Example from my production API:

I once built an API that only returned the first validation error. Developers would fix one issue, resubmit, and get another error. After ten rounds of this, they gave up. Learn from my mistake: return all errors at once.

400 Bad Request: Invalid JSON

Use case: Client sent malformed JSON that can’t be parsed.

HTTP Status: 400 Bad Request

{
  "error": {
    "code": "INVALID_JSON",
    "message": "Request body contains invalid JSON",
    "details": [
      {
        "message": "Unexpected token } in JSON at position 247",
        "line": 12,
        "column": 15
      }
    ],
    "timestamp": "2025-01-27T19:05:00Z",
    "path": "/api/v1/orders",
    "requestId": "req_json_error_001"
  }
}

Key points:

  • Specific error message from JSON parser
  • Line/column if available
  • Don’t expose internal stack traces

Pro tip: Use our JSON Formatter to validate JSON before sending requests. This prevents these errors during development.

401 Unauthorized: Missing Authentication

Use case: Request lacks authentication credentials.

HTTP Status: 401 Unauthorized

Headers:

WWW-Authenticate: Bearer realm="api"
{
  "error": {
    "code": "AUTHENTICATION_REQUIRED",
    "message": "Authentication credentials are required",
    "details": [
      {
        "message": "Missing Authorization header",
        "expectedFormat": "Authorization: Bearer <token>"
      }
    ],
    "timestamp": "2025-01-27T19:10:00Z",
    "path": "/api/v1/users/profile",
    "requestId": "req_auth_001"
  }
}

Alternative: Invalid token

{
  "error": {
    "code": "INVALID_TOKEN",
    "message": "Authentication token is invalid or expired",
    "details": [
      {
        "message": "JWT signature verification failed",
        "expiredAt": "2025-01-27T18:00:00Z"
      }
    ],
    "timestamp": "2025-01-27T19:15:00Z",
    "path": "/api/v1/users/profile",
    "requestId": "req_auth_002"
  }
}

Security note: Don’t expose too much detail about WHY authentication failed. Keep it vague to prevent attackers from gaining information.

Use our JWT Decoder to debug token issues during development.

403 Forbidden: Insufficient Permissions

Use case: User is authenticated but lacks permission for this action.

HTTP Status: 403 Forbidden

{
  "error": {
    "code": "INSUFFICIENT_PERMISSIONS",
    "message": "You do not have permission to perform this action",
    "details": [
      {
        "resource": "users",
        "action": "delete",
        "required": ["admin"],
        "current": ["user", "editor"]
      }
    ],
    "timestamp": "2025-01-27T19:20:00Z",
    "path": "/api/v1/users/12345",
    "requestId": "req_forbidden_001"
  }
}

Key points:

  • Clearly distinguish from 401 (authentication vs authorization)
  • Optionally specify required permissions
  • Don’t reveal sensitive info about the resource

Real-world scenario:

A regular user tries to delete another user’s account. They’re authenticated (have a valid token) but not authorized (don’t have admin role).

404 Not Found: Resource Doesn’t Exist

Use case: Requested resource doesn’t exist in the database.

HTTP Status: 404 Not Found

{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "The requested resource was not found",
    "details": [
      {
        "resource": "user",
        "identifier": "12345",
        "message": "No user exists with ID 12345"
      }
    ],
    "timestamp": "2025-01-27T19:25:00Z",
    "path": "/api/v1/users/12345",
    "requestId": "req_notfound_001"
  }
}

Alternative: Generic message

{
  "error": {
    "code": "NOT_FOUND",
    "message": "The requested resource was not found"
  }
}

Sometimes you want to be vague to avoid leaking information about what exists in your system.

Security consideration: Don’t use 404 for unauthorized access to resources that exist. Use 403 instead. Otherwise, attackers can enumerate resources by checking status codes.

429 Too Many Requests: Rate Limit Exceeded

Use case: Client exceeded API rate limits.

HTTP Status: 429 Too Many Requests

Headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706382600
Retry-After: 3600
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded",
    "details": [
      {
        "limit": 100,
        "remaining": 0,
        "resetAt": "2025-01-27T20:30:00Z",
        "retryAfter": 3600,
        "message": "You have exceeded the rate limit of 100 requests per hour"
      }
    ],
    "timestamp": "2025-01-27T19:30:00Z",
    "path": "/api/v1/users",
    "requestId": "req_ratelimit_001"
  }
}

Best practices:

  • Include rate limit info in headers AND body
  • Provide exact reset time
  • Use Retry-After header (in seconds)
  • Consider different limits for different endpoints

Example rate limit headers from GitHub API:

X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1706382600
X-RateLimit-Used: 1

GitHub’s approach is excellent—they tell you exactly how many requests you have left and when the limit resets.

500 Internal Server Error: Something Broke

Use case: Unexpected server error (database down, null pointer exception, etc.)

HTTP Status: 500 Internal Server Error

{
  "error": {
    "code": "INTERNAL_SERVER_ERROR",
    "message": "An unexpected error occurred",
    "details": [
      {
        "message": "We're working to fix this issue. Please try again later.",
        "support": "If this persists, contact support@example.com"
      }
    ],
    "timestamp": "2025-01-27T19:35:00Z",
    "path": "/api/v1/orders",
    "requestId": "req_error_001"
  }
}

What NOT to include:

  • Stack traces
  • Database error messages
  • File paths
  • Internal variable names

What to do instead:

  • Log the full error server-side
  • Return a generic message to the client
  • Provide a request ID for support
  • Monitor these errors closely

Real production story:

Early in my career, I returned database error messages directly to clients. This exposed table names, column names, and even parts of SQL queries. A security audit flagged it as a major vulnerability. Now I always log errors server-side and return generic messages.

503 Service Unavailable: Temporary Outage

Use case: Server is temporarily down for maintenance or overloaded.

HTTP Status: 503 Service Unavailable

Headers:

Retry-After: 3600
{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "The service is temporarily unavailable",
    "details": [
      {
        "reason": "Scheduled maintenance",
        "estimatedDuration": 3600,
        "resumeAt": "2025-01-27T21:00:00Z",
        "message": "We're performing scheduled maintenance. The service will be back online at 9:00 PM UTC."
      }
    ],
    "timestamp": "2025-01-27T20:00:00Z",
    "requestId": "req_maintenance_001"
  }
}

Best practices:

  • Set Retry-After header
  • Provide estimated downtime
  • Explain the reason if possible
  • Include a status page URL

Pagination Responses

Pagination is essential for any API returning lists. Here are three common patterns.

Offset-Based Pagination

Use case: Simple pagination like “Show me items 20-40.”

Request: GET /api/v1/products?limit=20&offset=20

HTTP Status: 200 OK

{
  "data": [
    {
      "id": 1021,
      "name": "Product 21",
      "price": 29.99
    },
    {
      "id": 1022,
      "name": "Product 22",
      "price": 34.99
    }
  ],
  "pagination": {
    "total": 250,
    "count": 20,
    "limit": 20,
    "offset": 20,
    "hasMore": true,
    "links": {
      "first": "/api/v1/products?limit=20&offset=0",
      "prev": "/api/v1/products?limit=20&offset=0",
      "next": "/api/v1/products?limit=20&offset=40",
      "last": "/api/v1/products?limit=20&offset=240"
    }
  }
}

Key fields:

  • total: Total items across all pages
  • count: Items in current response
  • limit: Items per page
  • offset: Starting position
  • hasMore: Whether more pages exist
  • links: URLs for navigation

Pros:

  • Simple to implement
  • Client can jump to any page
  • Total count available

Cons:

  • Performance degrades with high offsets
  • Inconsistent if data changes during pagination

Cursor-Based Pagination

Use case: Infinite scrolling, real-time feeds, large datasets.

Request: GET /api/v1/posts?limit=20&cursor=eyJpZCI6MTAyMH0

HTTP Status: 200 OK

{
  "data": [
    {
      "id": 1021,
      "title": "Understanding REST APIs",
      "author": "Sarah Chen",
      "publishedAt": "2025-01-20T10:00:00Z",
      "views": 1520
    },
    {
      "id": 1022,
      "title": "JSON Best Practices",
      "author": "Mike Johnson",
      "publishedAt": "2025-01-21T14:30:00Z",
      "views": 890
    }
  ],
  "pagination": {
    "count": 20,
    "nextCursor": "eyJpZCI6MTA0MH0",
    "prevCursor": "eyJpZCI6MTAwMH0",
    "hasMore": true
  }
}

Key fields:

  • nextCursor: Opaque token for next page
  • prevCursor: Token for previous page
  • hasMore: Whether more items exist

Cursor format:

Cursors are typically Base64-encoded JSON:

// Encode
const cursor = Buffer.from(JSON.stringify({id: 1020})).toString('base64');
// Result: eyJpZCI6MTAyMH0

// Decode
const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString());
// Result: {id: 1020}

Pros:

  • Consistent results even if data changes
  • Better performance for large offsets
  • Scales to massive datasets

Cons:

  • Can’t jump to arbitrary pages
  • No total count
  • More complex to implement

Use our Encoder tool to work with Base64 cursors.

Page-Based Pagination

Use case: Traditional pagination with page numbers.

Request: GET /api/v1/users?page=3&per_page=20

HTTP Status: 200 OK

{
  "data": [
    {
      "id": 12041,
      "username": "user41",
      "email": "user41@example.com"
    },
    {
      "id": 12042,
      "username": "user42",
      "email": "user42@example.com"
    }
  ],
  "pagination": {
    "currentPage": 3,
    "perPage": 20,
    "totalPages": 50,
    "totalItems": 1000,
    "hasNextPage": true,
    "hasPreviousPage": true,
    "links": {
      "first": "/api/v1/users?page=1&per_page=20",
      "prev": "/api/v1/users?page=2&per_page=20",
      "next": "/api/v1/users?page=4&per_page=20",
      "last": "/api/v1/users?page=50&per_page=20"
    }
  }
}

Key fields:

  • currentPage: Current page number (1-indexed)
  • perPage: Items per page
  • totalPages: Total number of pages
  • totalItems: Total items across all pages

Pros:

  • Intuitive for users
  • Easy to implement
  • Shows total pages

Cons:

  • Same issues as offset-based for large datasets
  • Total count can be expensive to calculate

Related Articles

Continue learning with these related posts

Found This Guide Helpful?

Try our free developer tools that power your workflow. No signup required, instant results.

Share This Article

Help others discover this guide

Share:

Stay Updated

Get notified about new guides and tools