InsightSocial API

Testing your integration

Test against the live API without surprises. What is free, what is billed, and which paths to check before launch.

Begin with a call that costs nothing. It proves your key works and records your starting balance before any billed test.

cURL
curl --include "https://api.insightsocial.app/v1/credits" \
  -H "x-api-key: $INSIGHTSOCIAL_API_KEY"
Response
{
  "success": true,
  "credits_used": 0,
  "credits_remaining": 500,
  "plan": { "tier": "free", "credits_per_month": 500, "resets_at": "2026-10-01T00:00:00.000Z" },
  "usage": { "window_start": "2026-09-01T00:00:00.000Z", "used": 0, "used_export": 0, "used_api": 0 },
  "pack_credits": 0,
  "free_calls": { "remaining": 10, "total": 10 },
  "key": { "id": "3f6c2a8e-7b41-4d0f-9a52-1c8e5d7b9f20", "name": "staging" },
  "request_id": "req_1a2b3c4d5e6f"
}

Check for status 200, success: true and credits_used: 0. Keep the request_id in your test log.

There is no sandbox

Every test runs against the real API, with the same prices, caching and rate limits as production.

Keys come in two forms, isk_live_… and isk_test_…. They behave identically: an isk_test_ key reaches the same data and spends real credits from the same balance. The prefix is a label for your own bookkeeping, not a test mode.

Use fixtures for parser tests that must be deterministic, and a small number of budgeted live calls to prove the end-to-end path works today. Neither proves that a source will be available tomorrow.

Use a separate test key

Create a key just for development or staging at Dashboard → API keys and keep it apart from production. Store it as INSIGHTSOCIAL_API_KEY in a server-side secret. Keep it out of source control, browser code, snapshots and logs.

A separate key lets you revoke it without touching production, and it has its own rate limits. It does not have its own budget: keys cannot be capped, and every key spends from the account balance. Record the balance before and after each test run, and revoke the key if it ever shows up in test output. See Authentication.

Free checks first

These cost nothing:

CheckWhat it proves
GET /v1/endpointsThe catalogue, prices and parameters. Needs no key, so it does not test yours.
GET /v1/creditsYour key is valid. Returns balance, plan and free calls left.
dry_run=1 on any data endpointReturns the price range in data.dry_run (credits_min, credits_max) without running the call. credits_max includes any max_pages you send. charge_reason is dry_run.
Any call that returns an errorErrors are never charged. That includes 400 UNSUPPORTED_PARAMETER, so sending a parameter schema 2 refuses is a free way to find it.

A dry_run=1 call looks like this:

Response
{
  "success": true,
  "platform": "instagram",
  "endpoint": "/v1/instagram/profile/posts",
  "schema_version": "2",
  "data": { "dry_run": { "credits_min": 10, "credits_max": 130 } },
  "unavailable": [],
  "credits_used": 0,
  "credits_remaining": 480,
  "request_id": "req_2ac2c2b769ef",
  "cached": false,
  "idempotent_replay": false,
  "charge_reason": "dry_run",
  "free_call": false
}

Then make one real call to check auth, transport and your parser against the live service:

cURL
curl --include "https://api.insightsocial.app/v1/instagram/profile?handle=natgeo" \
  -H "x-api-key: $INSIGHTSOCIAL_API_KEY"

This endpoint costs 10 credits. On a new account it may use one of your 10 lifetime free calls instead (free_call: true, credits_used: 0). Read credits_used and charge_reason rather than assuming what was billed, and keep live calls out of unit tests and pull-request loops. Check that the body says "schema_version": "2": that is the contract these docs describe. See Schema 2.

Try calls in the Playground

The Playground in your dashboard runs any endpoint from the browser, signed in as you, with no key to paste. Use it to see real responses and prices before you write code. Its calls are real and are charged exactly like API calls.

Test fixtures, nulls and unavailable fields

Keep small response fixtures in your own test suite, with keys and unneeded personal data removed. This one is trimmed from a real X response and covers the states a parser must tell apart:

Success fixture
{
  "success": true,
  "platform": "twitter",
  "endpoint": "/v1/twitter/user/tweets",
  "schema_version": "2",
  "data": {
    "items": [
      {
        "post": {
          "id": "2106502020849127865",
          "url": "https://x.com/NASA/status/2106502020849127865",
          "kind": null,
          "content": { "text": "RT @Astro_Jessica: Room with a view. …", "media_urls": null, "thumbnail_url": null, "duration_seconds": null },
          "author": { "id": null, "username": "NASA", "display_name": "NASA", "avatar_url": "https://pbs.twimg.com/…", "verified": true },
          "engagement": { "views": 40169, "likes": 0, "comments": 0, "shares": 572, "saves": 0 },
          "flags": { "nsfw": null, "spoiler": null, "pinned": false, "deleted": false, "likes_hidden": null, "comments_hidden": null, "shares_hidden": null, "views_hidden": null, "saves_hidden": null },
          "published_at": "2026-10-03T21:49:53.000Z",
          "language": "en",
          "ext": { "author_followers": 92378243, "quote_count": 0 }
        }
      }
    ],
    "total": null,
    "truncated": null
  },
  "pagination": { "next_cursor": "v2c.M-GEP84JXNZPqhQ0…", "has_more": true, "page_size": 19 },
  "unavailable": ["items[].post.author.id"],
  "credits_used": 10,
  "credits_remaining": 490,
  "request_id": "req_161a29ec13ca",
  "cached": false,
  "idempotent_replay": false,
  "charge_reason": "miss",
  "free_call": false
}

Assert each case on its own:

  1. schema_version is "2". Fail the test on anything else, so a key that is still on the old shape is caught early.
  2. A present value, such as post.author.username.
  3. A null, such as post.kind here: the field exists but this source did not say. null means "not known"; 0 means zero.
  4. A field listed in unavailable. Here items[].post.author.id is null on every row of the page, and the response says so. Record unavailable per call instead of judging completeness from nulls.
  5. ext: platform-specific keys in snake_case. Check that a key exists before reading it.
  6. Ids are strings, even when they look like numbers. Keep them as strings: several platforms use ids above 2^53.
  7. Times are ISO 8601 in UTC, ending in Z.
  8. post.kind is one of text, image, carousel, video, short, live, article, link, or null. New values may be added, so your code needs a default branch.

See Response schema for the full envelope.

For lists, test a one-page and a multi-page walk: send pagination.next_cursor back unchanged as cursor and stop on has_more: false. See Pagination.

Keep error fixtures too, and branch on error.type:

Error fixture
{
  "success": false,
  "error": {
    "type": "RATE_LIMITED",
    "message": "Too many requests on this key. Honour Retry-After, then back off with jitter."
  },
  "request_id": "req_9a8b7c6d5e4f",
  "credits_used": 0,
  "credits_remaining": null
}

Add one for every error type you handle: at least INVALID_API_KEY, INSUFFICIENT_CREDITS, RATE_LIMITED, CONCURRENCY_LIMIT and SERVICE_UNAVAILABLE. Schema 2 adds five more worth a fixture each:

error.typeStatusWhen
UNSUPPORTED_PARAMETER400A parameter that asked for analysis. error.param names it. Nothing is charged.
CURSOR_INVALID400The cursor was not issued by this API, is malformed, or belongs to a different request.
CURSOR_EXPIRED400The cursor is more than 24 hours old. Start again without one.
UPSTREAM_INVALID503The source returned a body we could not serve. Nothing is charged. Retry.
IDEMPOTENCY_KEY_REUSED409An Idempotency-Key reused with different parameters, or across schemas.
UNSUPPORTED_PARAMETER fixture
{
  "success": false,
  "error": {
    "type": "UNSUPPORTED_PARAMETER",
    "message": "`relevance` is not supported: it asks for analysis, which is not part of schema 2. Remove it and retry. Nothing was charged.",
    "param": "relevance"
  },
  "request_id": "req_80d197121a1e",
  "credits_used": 0,
  "credits_remaining": null
}
``` See [Error handling](/docs/errors) and the retry wrapper in the [Production checklist](/docs/production-checklist).

## Test repeat calls and forced fetches

Send the same call twice:

```bash title="cURL"
curl "https://api.insightsocial.app/v1/instagram/profile?handle=natgeo" \
  -H "x-api-key: $INSIGHTSOCIAL_API_KEY"

curl "https://api.insightsocial.app/v1/instagram/profile?handle=natgeo" \
  -H "x-api-key: $INSIGHTSOCIAL_API_KEY"

Each call may come back as a miss at the full price, or as shared_cache (cached: true) at 2 credits. Both calls are charged: a repeat is a new call. Assert on the charge_reason and credits_used you actually receive. See Caching.

Test a forced fetch only when you need one, and budget it at the endpoint's full price:

cURL
curl "https://api.insightsocial.app/v1/instagram/profile?handle=natgeo" \
  -H "x-api-key: $INSIGHTSOCIAL_API_KEY" \
  -H "Cache-Control: no-cache"

A successful forced fetch skips the shared cache: assert charge_reason: "miss" and the normal charge.

Test retries with an Idempotency-Key

Send the same call twice with one key:

cURL
IDEMPOTENCY_KEY="$(uuidgen)"

curl "https://api.insightsocial.app/v1/instagram/profile?handle=nasa" \
  -H "x-api-key: $INSIGHTSOCIAL_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY"

curl "https://api.insightsocial.app/v1/instagram/profile?handle=nasa" \
  -H "x-api-key: $INSIGHTSOCIAL_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY"

The second response should have idempotent_replay: true, charge_reason: "replay" and credits_used: 0. If the second request arrives while the first is still running, expect 409 IDEMPOTENCY_IN_PROGRESS; wait for Retry-After and send it again. Use a new key for every distinct call. A key first used with one schema answers 409 IDEMPOTENCY_KEY_REUSED when reused with the other. See Production checklist.

Pre-production checklist

  • The test key is separate from production, and the balance is recorded before and after each run.
  • The suite lists each endpoint, its current price, and the most calls it will make.
  • Free checks (/v1/credits, /v1/endpoints, dry_run=1) run before any billed call.
  • Every response is checked for "schema_version": "2".
  • Success and error fixtures cover values, null, absent ext keys, unavailable entries, string ids, ISO times and an unknown kind.
  • No request sends a parameter that schema 2 refuses. See Schema 2.
  • Pagination tests stop on has_more: false, return the cursor unchanged with the same parameters, and restart on CURSOR_EXPIRED.
  • Retry tests branch on error.type, use bounded delays, and resend the same cursor.
  • Repeat-call tests check charge_reason, cached and credits_used.
  • Forced fetches are budgeted as billed calls.
  • Idempotency tests replay one identical call and use a new key for anything else.
  • Logs tie together status, request_id, error.type, charge reason, credits and attempt count.
  • Production keys never enter the test environment or its output.

Next steps

On this page