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 --include "https://api.insightsocial.app/v1/credits" \
-H "x-api-key: $INSIGHTSOCIAL_API_KEY"{
"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:
| Check | What it proves |
|---|---|
GET /v1/endpoints | The catalogue, prices and parameters. Needs no key, so it does not test yours. |
GET /v1/credits | Your key is valid. Returns balance, plan and free calls left. |
dry_run=1 on any data endpoint | Returns 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 error | Errors 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:
{
"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 --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": 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:
schema_versionis"2". Fail the test on anything else, so a key that is still on the old shape is caught early.- A present value, such as
post.author.username. - A
null, such aspost.kindhere: the field exists but this source did not say.nullmeans "not known";0means zero. - A field listed in
unavailable. Hereitems[].post.author.idisnullon every row of the page, and the response says so. Recordunavailableper call instead of judging completeness from nulls. ext: platform-specific keys in snake_case. Check that a key exists before reading it.- Ids are strings, even when they look like numbers. Keep them as strings: several platforms use ids above 2^53.
- Times are ISO 8601 in UTC, ending in
Z. post.kindis one oftext,image,carousel,video,short,live,article,link, ornull. 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:
{
"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.type | Status | When |
|---|---|---|
UNSUPPORTED_PARAMETER | 400 | A parameter that asked for analysis. error.param names it. Nothing is charged. |
CURSOR_INVALID | 400 | The cursor was not issued by this API, is malformed, or belongs to a different request. |
CURSOR_EXPIRED | 400 | The cursor is more than 24 hours old. Start again without one. |
UPSTREAM_INVALID | 503 | The source returned a body we could not serve. Nothing is charged. Retry. |
IDEMPOTENCY_KEY_REUSED | 409 | An Idempotency-Key reused with different parameters, or across schemas. |
{
"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 "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:
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, absentextkeys,unavailableentries, string ids, ISO times and an unknownkind. - 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 onCURSOR_EXPIRED. - Retry tests branch on
error.type, use bounded delays, and resend the same cursor. - Repeat-call tests check
charge_reason,cachedandcredits_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.