Home / Blog / Amazon Product API in C: libcurl Client, json-c Decoding and Retries

Amazon Product API in C: libcurl Client, json-c Decoding and Retries

Call an Amazon product API from C with libcurl and json-c: one client with the key in an X-Api-Key header and a 120-second CURLOPT_TIMEOUT (libcurl's own default is 0, meaning never), NULL-safe decoding for a price that is a number or the string "N/A", the statistics price in integer cents kept out of a decimal lookup price, and retries that never pay twice for the same billed answer.

November 07, 2026
8 min read
SellerMagnet Team
Share & Bookmark
Terminal showing a C struct for the SellerMagnet API client with a 120 second libcurl timeout and the statistics price kept in integer cents

To call an Amazon product API from C, send a GET request with libcurl to /api/amazon-product-lookup with an ASIN, a marketplace ID and your key in the X-Api-Key header, then decode the JSON body with json-c. This quickstart builds one client for three endpoints (lookup, offers and statistics), sets a timeout libcurl does not set for you by default, decodes a price that can be a number or the literal string "N/A" without crashing, and retries only the errors a second attempt can fix.

Key Takeaways

  • libcurl's CURLOPT_TIMEOUT defaults to 0, which means it never times out during a transfer. Set it to 120 seconds: the API can take that long, and an abandoned request is still billed.
  • Send the key in the X-Api-Key header through a curl_slist, never in the URL as api_key. A query parameter would win over the header if both were sent, and would also land in logs.
  • Lookup sends buyBoxInfo.price as a number or the string "N/A". A helper that checks the json-c type before reading a double keeps a missing price from becoming 0.0 or a decode error.
  • Statistics sends buyBoxPrice in integer minor units (41800 = 418.00); lookup and offers send decimals. Reading them through two different helper functions keeps the two from mixing.
  • Retry 429, 500 once, 502 and 503. A 404 is an answer: it is billed once, and a retry pays for the same answer again. libcurl itself never retries anything on its own.

Prerequisites: a C11 compiler, libcurl and json-c

  • Any C11 compiler (this guide was tested with GCC 13) plus the libcurl and json-c development headers (libcurl4-openssl-dev and libjson-c-dev on Debian/Ubuntu).
  • An API key: a free account includes 150 credits after e-mail verification, one credit per request.
  • The key in the SELLERMAGNET_API_KEY environment variable, so it never lands in your repository or in a hard-coded string.

Install the headers, save the five files, compile

gcc --version                                  # any C11 compiler; this guide was tested with GCC 13
sudo apt-get install -y libcurl4-openssl-dev libjson-c-dev   # or your distro's curl/json-c -dev packages
mkdir amazon-c && cd amazon-c
# save sellermagnet.h, sellermagnet.c, decode.h, decode.c, retry.h, retry.c and main.c from this post
export SELLERMAGNET_API_KEY="your-key-here"
gcc -std=c11 -O2 -o amazon-c main.c sellermagnet.c retry.c decode.c $(pkg-config --cflags --libs json-c libcurl)
./amazon-c                        # at most 3 credits per product, see the credit math below

How do I call the Amazon product API from C?

Build one request per call with curl_easy_init(); libcurl handles like this are not meant to be reused concurrently, so a short-lived handle per request is the simplest safe shape. The key goes into a curl_slist header, not the URL. The header matters for more than tidiness: when a request carries both, the api_key query parameter wins, so a stale key left in a URL overrides a valid header, and a URL with a key ends up in logs and error messages. The header vs query string guide shows where it leaks.

sellermagnet.h - the client, the result type, the ASIN check

/* One libcurl client: the key in the X-Api-Key header, a 120 s timeout, one decoded JSON envelope. */
#ifndef SELLERMAGNET_H
#define SELLERMAGNET_H

#include <json-c/json.h>

typedef struct {
    char base[256];
    char key[128];
    long timeout_s;
    long billed; /* number of requests the API charged, including retries */
} Client;

typedef struct {
    int ok;             /* 1 = success, out holds data */
    int status;         /* HTTP status, or -1 for a transport error (no answer at all) */
    char message[512];  /* the API's own message, or a transport error description */
    int retry_after;    /* seconds, -1 if the response carried no Retry-After */
    int is_transport;   /* 1 = curl never got an HTTP answer (DNS/connect/timeout) */
} ApiResult;

/* Reads SELLERMAGNET_API_KEY and SELLERMAGNET_API_BASE (defaults to the public base). Returns NULL
   and prints "set SELLERMAGNET_API_KEY" if the key is missing - the client never sends an empty key. */
Client *client_from_env(void);
void client_free(Client *c);

/* One GET, no retry. keys/vals are n parallel arrays of query parameters (asin, marketplaceId, ...).
   On success *out is a json_object the caller must json_object_put(); on failure *out is NULL and
   res carries the status/message. Every billed status (200/404/502/503) increments c->billed exactly
   once per call - the caller never has to track billing itself. */
int client_get_once(Client *c, const char *endpoint, const char *const *keys, const char *const *vals,
                     int n, struct json_object **out, ApiResult *res);

/* Returns 1 if s is a plausible Amazon ASIN (10 upper-case alphanumeric characters): checked before
   any call, because a malformed ASIN still costs a credit on lookup/offers and a billed 400 on
   statistics. Lower-case input is rejected, not upper-cased: the API is case-sensitive on ASIN. */
int is_asin(const char *s);

#endif

sellermagnet.c - one request, one decoded JSON envelope

#include "sellermagnet.h"

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strncasecmp */

#include <curl/curl.h>

struct buf {
    char *data;
    size_t len;
};

static size_t write_cb(void *ptr, size_t size, size_t nmemb, void *userdata) {
    struct buf *b = (struct buf *)userdata;
    size_t add = size * nmemb;
    char *grown = realloc(b->data, b->len + add + 1);
    if (!grown) return 0; /* out of memory: abort the transfer, curl reports CURLE_WRITE_ERROR */
    b->data = grown;
    memcpy(b->data + b->len, ptr, add);
    b->len += add;
    b->data[b->len] = '\0';
    return add;
}

struct hdr_ctx {
    int retry_after; /* -1 until a Retry-After header is seen */
};

static size_t header_cb(char *buffer, size_t size, size_t nitems, void *userdata) {
    struct hdr_ctx *h = (struct hdr_ctx *)userdata;
    size_t n = size * nitems;
    if (n > 11 && strncasecmp(buffer, "retry-after", 11) == 0) {
        const char *colon = memchr(buffer, ':', n);
        if (colon) h->retry_after = (int)strtol(colon + 1, NULL, 10);
    }
    return n;
}

int is_asin(const char *s) {
    if (!s || strlen(s) != 10) return 0;
    for (int i = 0; i < 10; i++) {
        if (!isupper((unsigned char)s[i]) && !isdigit((unsigned char)s[i])) return 0;
    }
    return 1;
}

Client *client_from_env(void) {
    const char *key = getenv("SELLERMAGNET_API_KEY");
    if (!key || !*key) {
        fprintf(stdout, "set SELLERMAGNET_API_KEY\n");
        return NULL;
    }
    Client *c = calloc(1, sizeof(Client));
    const char *base = getenv("SELLERMAGNET_API_BASE");
    snprintf(c->base, sizeof(c->base), "%s", base && *base ? base : "https://sellermagnet-api.com/api");
    snprintf(c->key, sizeof(c->key), "%s", key);
    c->timeout_s = 120; /* the API can take that long; curl's own default is a non-infinite one minute */
    c->billed = 0;
    return c;
}

void client_free(Client *c) { free(c); }

int client_get_once(Client *c, const char *endpoint, const char *const *keys, const char *const *vals,
                     int n, struct json_object **out, ApiResult *res) {
    *out = NULL;
    memset(res, 0, sizeof(*res));
    res->retry_after = -1;

    CURL *curl = curl_easy_init();
    if (!curl) {
        res->is_transport = 1;
        res->status = -1;
        snprintf(res->message, sizeof(res->message), "curl_easy_init failed");
        return -1;
    }

    char url[1024];
    snprintf(url, sizeof(url), "%s/%s", c->base, endpoint);
    for (int i = 0; i < n; i++) {
        char *esc = curl_easy_escape(curl, vals[i], 0);
        char piece[320];
        snprintf(piece, sizeof(piece), "%c%s=%s", i == 0 ? '?' : '&', keys[i], esc);
        curl_free(esc);
        strncat(url, piece, sizeof(url) - strlen(url) - 1);
    }

    struct curl_slist *headers = NULL;
    char auth[160];
    /* the key goes in a header, never in the URL as api_key - a query parameter would win over it
       if both were sent, and would also land in access logs; see the header-vs-query post. */
    snprintf(auth, sizeof(auth), "X-Api-Key: %s", c->key);
    headers = curl_slist_append(headers, auth);

    struct buf body = {0};
    struct hdr_ctx hc = {-1};
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb);
    curl_easy_setopt(curl, CURLOPT_HEADERDATA, &hc);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, c->timeout_s); /* the whole request, body included */

    CURLcode rc = curl_easy_perform(curl);
    if (rc != CURLE_OK) {
        res->is_transport = 1;
        res->status = -1;
        snprintf(res->message, sizeof(res->message), "transport: %s", curl_easy_strerror(rc));
        curl_slist_free_all(headers);
        curl_easy_cleanup(curl);
        free(body.data);
        return -1;
    }

    long status = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
    res->status = (int)status;
    res->retry_after = hc.retry_after;
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);

    /* every status the API charges for (200/404/502/503) is one billed call, counted here so the
       caller cannot forget a code path and under- or over-count credits. */
    if (status == 200 || status == 404 || status == 502 || status == 503) c->billed++;
    if (status == 500) { /* refunded since 2026-09-19, but still a real HTTP attempt */ }

    struct json_object *root = body.data ? json_tokener_parse(body.data) : NULL;
    free(body.data);
    if (!root || !json_object_is_type(root, json_type_object)) {
        /* the per-IP limiter answers a 429 with an HTML page, not JSON: treat any non-JSON body as
           an API error with its raw status instead of crashing the parser. */
        if (root) json_object_put(root);
        res->ok = (status == 200);
        if (!res->ok) snprintf(res->message, sizeof(res->message), "non-JSON response (status %ld)", status);
        return res->ok ? 0 : -1;
    }

    struct json_object *success = NULL, *data = NULL, *message = NULL;
    json_object_object_get_ex(root, "success", &success);
    json_object_object_get_ex(root, "data", &data);
    json_object_object_get_ex(root, "message", &message);

    if (success && json_object_get_boolean(success)) {
        *out = json_object_get(data); /* take our own reference before root is freed */
        json_object_put(root);
        res->ok = 1;
        return 0;
    }
    snprintf(res->message, sizeof(res->message), "%s", message ? json_object_get_string(message) : "error");
    json_object_put(root);
    res->ok = 0;
    return -1;
}

client_get_once returns the decoded data object or fills an ApiResult with the status, the API's own message and any Retry-After. Every JSON body is {"success": ..., "data": ...} on success or carries a message on failure. The per-IP limit (600 requests a minute, 15,000 an hour) answers 429 with an HTML page and no Retry-After, which is why a body that is not valid JSON still becomes an API error instead of crashing json_tokener_parse. c->billed counts the attempts the API actually charges, retries included, so the caller never has to track credits itself.

How do I decode a price that is a number or "N/A"?

Check the json-c type before reading the value. productInfo.buyBoxInfo.price is the featured offer's item price without shipping, in decimal units, or the string "N/A" when no offer is featured. Reading it with a plain json_object_get_double on a string value returns 0 without any error, which silently looks like a real price of zero. double_or_na only accepts a json_type_double or json_type_int value, so "N/A" and a missing key both read as "absent", never as 0.0.

decode.c - price fields that are a number or "N/A", and the cents conversion

#include "decode.h"

#include <string.h>

int double_or_na(struct json_object *obj, const char *key, double *out) {
    struct json_object *v;
    if (!obj || !json_object_object_get_ex(obj, key, &v)) return 0;
    if (json_object_is_type(v, json_type_double) || json_object_is_type(v, json_type_int)) {
        *out = json_object_get_double(v);
        return 1;
    }
    return 0; /* "N/A" or null */
}

int long_or_na(struct json_object *obj, const char *key, long *out) {
    struct json_object *v;
    if (!obj || !json_object_object_get_ex(obj, key, &v)) return 0;
    if (json_object_is_type(v, json_type_int)) {
        *out = json_object_get_int64(v);
        return 1;
    }
    return 0;
}

double minor_to_decimal(long minor, const char *marketplace_id) {
    return strcmp(marketplace_id, "A1VC38T7YXB528") == 0 ? (double)minor : (double)minor / 100.0;
}

const char *obj_str(struct json_object *obj, const char *key, const char *def) {
    struct json_object *v;
    if (obj && json_object_object_get_ex(obj, key, &v) && json_object_is_type(v, json_type_string))
        return json_object_get_string(v);
    return def;
}

Trimmed lookup response (example values from the API docs)

{
  "success": true,
  "data": {
    "productInfo": {
      "asin": "B0CL61F39H",
      "buyBoxInfo": {"price": 444.99, "currencyCode": "USD", "currencySymbol": "$"}
    }
  }
}

The offers endpoint returns the live Buy Box and the first page of offers. buyBox is an empty JSON object when nobody wins it; json_object_object_get_ex on an empty object simply returns 0 (not found) for totalPrice, so double_or_na reads it as absent without any special-casing. An offer whose item price could not be read can arrive with totalPrice 0.0, so the row logic also checks total > 0.0 before treating a decoded number as a real price. inventory is the add-to-cart maximum, not the seller's stock.

Why is the statistics price 100 times bigger?

Because /amazon-product-statistics reports buyBoxPrice in integer minor units: 41800 means 418.00. Lookup and offers report decimal units. It is also a landed price (item plus shipping, like the offers totalPrice), while the lookup price excludes shipping, so 409.99 from lookup and 41800 from statistics can describe the same offer. minor_to_decimal divides by 100, except on amazon.co.jp, where the values are whole yen. More detail is in the cents vs decimal guide.

JSON fields and the C decoding helpers that read them safely (November 2026).
FieldEndpointJSON valueC handling
buyBoxInfo.pricelookup444.99 or "N/A"double_or_na() -> int 0/1 + double
buyBoxoffers{} when noneNULL-safe json_object *, checked before use
buyBox.totalPriceoffersnot roundeddouble, format with %.2f once
offers[].totalPriceoffers418.00double
offers[].inventoryoffers30 (cart cap)long
buyBoxPricestatistics41800 or "N/A"long_or_na() -> int 0/1 + long
buyBoxFulfillmentstatistics"FBM" or nullconst char * or NULL
Table mapping SellerMagnet API price fields to the C decoding helper that reads each one safely, with the long_or_na helper for the statistics price in cents
Decimal on lookup and offers, cents on statistics: one decoding helper per format.

Retry policy: which errors a C client should retry

Retry only what can succeed on a second try. A 429 from the per-key concurrency cap is free and carries Retry-After: 2. A 500 without Retry-After is refunded, so one retry costs nothing beyond the retry itself; a 500 that carries Retry-After stays charged. 502 and 503 are billed on every attempt, so wait at least the Retry-After and cap the attempts. Statistics covers 11 marketplaces; anywhere else it answers a free 400. A timeout is not retried the same way as a billed error: the first request may still be running on the server, and every attempt against a real server is billed, so treat a transport-level timeout the same as a connect failure, not the same as a 502.

Status codes, billing and the right C client reaction (November 2026).
StatusMeaningChargedClient action
200SuccessYesDecode data
400Bad parameter or uncovered marketplaceOnly an endpoint's own 400Fix the call
401 / 403Key or creditsNoStop the run
404Not on this marketplaceYesRecord, never retry
429Concurrency or per-IP limitNoWait Retry-After
500Internal error, no Retry-AfterNo, refundedRetry once
502 / 503Retryable error / unavailableYesWait, retry, cap
TimeoutLibcurl never times out by defaultYesSet CURLOPT_TIMEOUT, never resend

retry.c - retries that respect billing

#include "retry.h"

#include <stdio.h>
#include <unistd.h>

#define ATTEMPTS 4

/* std::thread::sleep equivalent: production sleeps for real, the test harness overrides this
   function pointer so the scenario tests run in milliseconds instead of minutes. */
void (*retry_sleep)(int seconds) = NULL;

static void do_sleep(int seconds) {
    if (retry_sleep) retry_sleep(seconds);
    else sleep((unsigned)seconds);
}

static int retryable(int status) { return status == 429 || status == 500 || status == 502 || status == 503; }

static int backoff(int attempt) { return attempt; } /* 1, 2, 3 s before the capped attempt */

int get_with_retry(Client *c, const char *endpoint, const char *const *keys, const char *const *vals,
                    int n, struct json_object **out, ApiResult *res) {
    int internal_errors = 0;
    for (int attempt = 1; attempt <= ATTEMPTS; attempt++) {
        int rc = client_get_once(c, endpoint, keys, vals, n, out, res);
        if (rc == 0) return 0; /* success */

        int wait = -1;
        if (!res->is_transport && retryable(res->status)) {
            if (res->status == 500) {
                internal_errors++;
                if (internal_errors > 1) return -1; /* a second 500 for the same call: report it */
            }
            wait = backoff(attempt);
            if (res->retry_after > wait) wait = res->retry_after;
        } else if (res->is_transport) {
            wait = backoff(attempt); /* connect/timeout: never reached the API on connect failures */
        } else {
            return -1; /* 400/401/403/404: an answer, retrying would only pay for it again */
        }
        if (attempt == ATTEMPTS) return -1;
        fprintf(stderr, "%s: %d: %s (retry %d/%d in %d s)\n", endpoint, res->status, res->message,
                attempt, ATTEMPTS - 1, wait);
        do_sleep(wait);
    }
    return -1;
}

libcurl has no retry layer of its own: a failed curl_easy_perform call is never resent automatically, unlike reqwest's narrow HTTP/2 GOAWAY retry in the Rust quickstart. This loop decides what gets sent twice, and nothing else does. For 502 handling on the offers endpoint in particular, see the 502 retry guide.

Sequence of a C client retrying a free 429 after Retry-After, then recording a billed 404 without retrying
Wait and retry a free 429; record a billed 404 and move on.

Run lookup, offers and statistics for five rows

main.c checks each ASIN's format first (case-sensitive, 10 upper-case alphanumeric characters), because a malformed ASIN still costs a credit on lookup and offers, and a billed 400 on statistics. It then calls the three endpoints, stops the whole run on 401 or 403 and prints one row per product. A 404 ends its row after one credit, without calling offers and statistics.

main.c - three endpoints per product, one row each

/* lookup, offers and statistics for five products: one row each, billing-aware retries, typed-ish
   price decoding (a number or the literal string "N/A" never crashes a naive atof). */
#include <stdio.h>
#include <string.h>

#include "decode.h"
#include "retry.h"
#include "sellermagnet.h"

typedef struct {
    const char *asin;
    const char *marketplace;
} Product;

/* a product costs up to 3 credits: lookup, offers and statistics */
static const Product PRODUCTS[] = {
    {"B0CL61F39H", "ATVPDKIKX0DER"},  /* amazon.com */
    {"B0CLTBHXWQ", "APJ6JRA9NG5V4"},  /* amazon.it */
    {"B0CL61F39H", "A2VIGQ35RCS4UG"}, /* amazon.ae: statistics does not cover it (free 400) */
    {"B0NOTFOUND", "ATVPDKIKX0DER"},  /* not on this marketplace: billed 404, never retried */
    {"B0CL61F39", "ATVPDKIKX0DER"},   /* 9 characters: skipped before it costs a credit */
};
#define N_PRODUCTS (int)(sizeof(PRODUCTS) / sizeof(PRODUCTS[0]))

/* Returns a heap line (caller frees) for one product, or NULL on a stop condition the caller
   already printed (401/403: every further call would fail the same way). */
static int row(Client *c, const char *asin, const char *market, char *line, size_t cap, int *stop) {
    *stop = 0;
    if (!is_asin(asin)) {
        snprintf(line, cap, "not an ASIN, skipped (0 credits)");
        return 0;
    }

    struct json_object *lookup = NULL;
    ApiResult res;
    const char *keys[] = {"asin", "marketplaceId"};
    const char *vals[] = {asin, market};
    if (get_with_retry(c, "amazon-product-lookup", keys, vals, 2, &lookup, &res) != 0) {
        if (res.status == 401 || res.status == 403) {
            fprintf(stderr, "stopping: %s\n", res.message);
            *stop = 1;
            return -1;
        }
        snprintf(line, cap, "%d: %.150s", res.status, res.message); /* e.g. 404: Invalid product */
        return 0;
    }
    struct json_object *info = NULL;
    json_object_object_get_ex(lookup, "productInfo", &info);
    struct json_object *bbi = NULL;
    if (info) json_object_object_get_ex(info, "buyBoxInfo", &bbi);
    double lookup_price;
    char lookup_cell[64];
    if (bbi && double_or_na(bbi, "price", &lookup_price))
        snprintf(lookup_cell, sizeof(lookup_cell), "%.2f %s", lookup_price, obj_str(bbi, "currencyCode", ""));
    else
        snprintf(lookup_cell, sizeof(lookup_cell), "N/A");
    json_object_put(lookup);

    struct json_object *offers = NULL;
    if (get_with_retry(c, "amazon-product-offers", keys, vals, 2, &offers, &res) != 0) {
        if (res.status == 401 || res.status == 403) {
            fprintf(stderr, "stopping: %s\n", res.message);
            *stop = 1;
            return -1;
        }
        snprintf(line, cap, "%-10s  %d: %.100s", lookup_cell, res.status, res.message);
        return 0;
    }
    struct json_object *bb = NULL, *cur = NULL;
    json_object_object_get_ex(offers, "buyBox", &bb);
    json_object_object_get_ex(offers, "currency", &cur); /* {"code": "USD", "name": ..., "symbol": ...} */
    char offer_cell[64];
    double total;
    /* buyBox is {} when nobody wins it, or an unread price can arrive as 0.0: neither is "a price". */
    if (bb && double_or_na(bb, "totalPrice", &total) && total > 0.0) {
        const char *seller = obj_str(bb, "sellerId", "");
        const char *ful = obj_str(bb, "fulfillmentType", "");
        snprintf(offer_cell, sizeof(offer_cell), "%.2f %s %s %s", total,
                 obj_str(cur, "code", ""), ful, seller);
    } else {
        snprintf(offer_cell, sizeof(offer_cell), "none, no new offers");
    }
    json_object_put(offers);

    struct json_object *stats = NULL;
    int stat_rc = get_with_retry(c, "amazon-product-statistics", keys, vals, 2, &stats, &res);
    char stat_cell[160];
    if (stat_rc != 0) {
        if (res.status == 401 || res.status == 403) {
            fprintf(stderr, "stopping: %s\n", res.message);
            *stop = 1;
            return -1;
        }
        if (res.status == 400)
            snprintf(stat_cell, sizeof(stat_cell), "not covered (free 400)");
        else
            snprintf(stat_cell, sizeof(stat_cell), "%d: %.100s", res.status, res.message);
    } else {
        long minor;
        const char *fulfillment = obj_str(stats, "buyBoxFulfillment", NULL);
        if (long_or_na(stats, "buyBoxPrice", &minor))
            snprintf(stat_cell, sizeof(stat_cell), "%ld = %.2f %s", minor, minor_to_decimal(minor, market),
                      fulfillment ? fulfillment : "");
        else
            snprintf(stat_cell, sizeof(stat_cell), "N/A");
        json_object_put(stats);
    }

    snprintf(line, cap, "%-10.10s  %-30.30s  %.190s", lookup_cell, offer_cell, stat_cell);
    return 0;
}

int main(void) {
    Client *c = client_from_env();
    if (!c) return 1;

    printf("%-10s  %-14s  %-10s  %-30s  statistics\n", "asin", "marketplace", "lookup", "offers buy box");
    for (int i = 0; i < N_PRODUCTS; i++) {
        char line[256];
        int stop;
        if (row(c, PRODUCTS[i].asin, PRODUCTS[i].marketplace, line, sizeof(line), &stop) != 0) {
            if (stop) { client_free(c); return 1; }
        }
        printf("%-10s  %-14s  %s\n", PRODUCTS[i].asin, PRODUCTS[i].marketplace, line);
    }
    printf("credits charged: %ld\n", c->billed);
    client_free(c);
    return 0;
}

Program output (illustrative data; the retry lines are stderr)

amazon-product-lookup: 429: Max 40 concurrent requests per API key. Retry shortly. (retry 1/3 in 2 s)
amazon-product-statistics: 500: An internal error occurred. Please try again shortly. (retry 1/3 in 1 s)
asin        marketplace     lookup      offers buy box                  statistics
B0CL61F39H  ATVPDKIKX0DER   444.99 USD  444.99 USD FBA A3853PJW50SJG8   44499 = 444.99 FBA
B0CLTBHXWQ  APJ6JRA9NG5V4   409.99 EUR  418.00 EUR FBM A2I59UVTUWUFH0   41800 = 418.00 FBM
B0CL61F39H  A2VIGQ35RCS4UG  N/A         none, no new offers             not covered (free 400)
B0NOTFOUND  ATVPDKIKX0DER   404: Invalid product
B0CL61F39   ATVPDKIKX0DER   not an ASIN, skipped (0 credits)
credits charged: 9

The credit math for that run: 12 requests, 9 credits. Three answers cost nothing: the 429, the 500 (refunded) and the statistics 400 on amazon.ae. The 404 cost one credit and ended its row, and the 9-character ASIN never left the program. A 502 or 503 would have cost a credit on every attempt. Note the second row: lookup says 409.99 (item price), offers and statistics say 418.00 (item plus 8.01 shipping, and 41800 in cents). To test changes without spending credits, point SELLERMAGNET_API_BASE at a local fake HTTP server; that is how this code was tested, with no network call ever reaching the real API.

Frequently Asked Questions

Is there a C SDK for the SellerMagnet API?

No, and none is needed. Every endpoint is a plain GET with query parameters and a JSON body, so libcurl for HTTP and json-c for decoding cover it in well under 300 lines.

Does libcurl time out on its own?

No. CURLOPT_TIMEOUT defaults to 0, which means the transfer never times out. Set it explicitly, and match it to how long the API itself can take (about 120 seconds) rather than guessing a short value that cuts off a slow but successful answer.

Why does a naive double read fail on buyBoxInfo.price?

Because the field is a number or the string "N/A" when no offer is featured. json_object_get_double on a string value returns 0.0 with no error, which looks exactly like a real zero price. Check the json-c type first.

What timeout should a C client use?

120 seconds. libcurl's own default is 0 (never), which is worse than a too-short guess: a client that never sets a timeout can hang forever on a stalled connection instead of failing fast.

Can one CURL handle be reused across threads?

Not concurrently. A libcurl easy handle is not thread-safe for simultaneous use; create one handle per request (as this client does) or give each thread its own handle.

Bottom line: one libcurl request per call with an explicit 120-second timeout and the key in an X-Api-Key header, json-c type checks that turn "N/A" into "absent" instead of 0.0, a separate conversion for the statistics price in cents, and retries for 429, 500, 502 and 503 only. The same client shape exists for Python, and every endpoint is in the API documentation.

Ready to Extract Amazon Data at Scale?

Start building with SellerMagnet API today. Real-time product data, competitive pricing, and review analytics at your fingertips.

150 free API credits • No credit card required • Cancel anytime