ResourceHub Document Library

ResourceHub API V1

Read, create, and delete ResourceHub resources from external systems and integrations.
On this pageBase URL
Reads return only public, active resources. Format: File is created in the background (HTTP 202 + poll GET /jobs/:id). Link and Video stay synchronous.

Base URL

https://resource-hub.gadget.app/api/v1

Replace the host if you are calling a development environment.

Authentication

Create an API key in ResourceHub Admin › Settings › API. The full secret is shown once. Enable Allow create and delete when the partner must write.

Authorization: Bearer rhk_xxxxxxxx_…

Use the full secret shown once when the key is created (not the prefix in the table). In Postman, set Authorization to Bearer Token and paste the full key. Keys start with rhk_ (legacy rhk_live_ keys still work). You can also send X-ResourceHub-Api-Key or ?api_key=.

The shop is taken from the key. Do not send a shop id. Shopify credentials are never exposed.

ScopeMethods
resources:readGET /resources, GET /resources/:id, GET /files/:id
resources:writePOST /resources, PATCH /resources/:id, POST /resources/bulk, POST /resources/bulk/uploads, GET /jobs/:id, DELETE /resources/:id, POST /resources/delete

Existing keys stay read-only until you create a new key with write enabled. Missing scope returns HTTP 403.

Limits

ControlValue
List / get60 requests / minute / key
File URL20 requests / minute / key
Create / update / delete20 requests / minute / key
Bulk create10 requests / minute / key, max 25 items
Bulk file uploadsPresigned PUT, 30 minutes; complete with the same job_id
Bulk deleteMax 50 ids
Direct file upload50 MB on POST /resources (multipart file)
Page sizeDefault 25, maximum 50
Over limitHTTP 429 with Retry-After

List requires at least one of sku, title, product_id, id, or updated_since.

Methods

GET /resources

Query public resources. Cursor pagination via first and after.

Query: sku, title, product_id, id, resource_type, updated_since, first, after

sku is an exact token match. title is a partial match on indexed title tokens.

curl -sS "https://resource-hub.gadget.app/api/v1/resources?sku=ABC-100" \
  -H "Authorization: Bearer rhk_…"
curl -sS "https://resource-hub.gadget.app/api/v1/resources?title=User%20Manual" \
  -H "Authorization: Bearer rhk_…"

GET /resources/:id

One resource by ResourceHub id. Includes page_content when the merchant filled full page HTML.

curl -sS "https://resource-hub.gadget.app/api/v1/resources/123456" \
  -H "Authorization: Bearer rhk_…"

GET /files/:id

Return the hosted S3 object URL for a public stored file (same file_url as GET /resources). Returns 404 for videos, external links, and private files.

curl -sS "https://resource-hub.gadget.app/api/v1/files/123456" \
  -H "Authorization: Bearer rhk_…"

POST /resources

Create one resource. Requires resources:write. title is required. external_id is ignored if sent.

Link and Video (JSON) return HTTP 201 with the resource. File returns HTTP 202 with a job; poll GET /jobs/:id. Send either a public link (JSON) or a local file (multipart, max 50 MB). Do not send both; file wins and sets format to File. Allowed extensions include pdf, jpg, jpeg, png, zip, docx, and the rest of the shop file types.

curl -sS -X POST "https://resource-hub.gadget.app/api/v1/resources" \
  -H "Authorization: Bearer rhk_…" \
  -F "title=Model 5308 User Manual" \
  -F "format=File" \
  -F "resource_type=Manual" \
  -F "language=en" \
  -F "status=active" \
  -F "file=@./manual.pdf"

JSON create with a remote link still works:

curl -sS -X POST "https://resource-hub.gadget.app/api/v1/resources" \
  -H "Authorization: Bearer rhk_…" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "Model 5308 User Manual",
  "format": "File",
  "link": "https://example.com/5308.pdf",
  "resource_type": "Manual",
  "language": "en",
  "status": "active",
  "categories": ["Manuals"],
  "tags": ["5308"],
  "product_skus": ["ABC-100"],
  "custom_fields": { "voltage": "24V" }
}'
{
  "data": {
    "id": "123",
    "status": "queued",
    "resource_id": null,
    "poll_url": "/api/v1/jobs/123"
  }
}

File with link requires http/https (not YouTube/Vimeo). Video requires a YouTube or Vimeo link (no file upload). CSV aliases: link / external_link, short_description / description, full_page_content / page_content, product_skus / skus. Types, formats, languages, SKUs, and custom field keys must already exist for the shop. Multipart lists may be JSON strings, e.g. categories=["Manuals"].

PATCH /resources/:id

Update one resource owned by this shop. Requires resources:write. Identify the resource with the ResourceHub id in the URL. Same sparse rules as CSV Import: omit a field or send it empty ("", []) and it is not written. Only keys with a non-empty value are applied. title is optional. format is ignored and never changed.

Metadata (title, status, categories, tags, SKUs, page content, …) and Video/Link URL changes return HTTP 200 with the resource. Replacing a File with a new link or multipart file returns HTTP 202 with a job; poll GET /jobs/:id. file is only allowed on File resources (same rules as POST). Associate products with product_skus or product_handles (not product_ids).

curl -sS -X PATCH "https://resource-hub.gadget.app/api/v1/resources/123456" \
  -H "Authorization: Bearer rhk_…" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "Model 5308 User Manual (rev B)",
  "status": "draft"
}'

Replace a hosted file:

curl -sS -X PATCH "https://resource-hub.gadget.app/api/v1/resources/123456" \
  -H "Authorization: Bearer rhk_…" \
  -F "file=@./manual-revb.pdf"

Empty strings, empty arrays, and missing keys are skipped (CSV present_columns). Sending categories or tags with values replaces the full set. Custom field keys present in custom_fields are upserted; other custom fields are left alone. Booleans like featured and no_index are written when sent as true or false.

GET /jobs/:id

Poll a File create, update, or bulk-upload job for this shop. Status: queued (including an upload session waiting for PUTs), processing, completed, completed_with_errors, failed. When completed, use resource_id (or items[].resource_id for bulk) with GET /resources/:id.

curl -sS "https://resource-hub.gadget.app/api/v1/jobs/123" \
  -H "Authorization: Bearer rhk_…"

POST /resources/bulk

Create up to 25 resources. Link and Video bulk stays HTTP 200 with partial success. If any item is File, the whole payload is one background job (HTTP 202).

File with a public URL still uses JSON link (no upload session). Local files must not be sent as multipart on this endpoint: start POST /resources/bulk/uploads, PUT each file to S3, then complete here with job_id and upload_id (or the echoed file_url). Mix Link items and staged Files in the same complete call. Completing twice returns HTTP 409.

curl -sS -X POST "https://resource-hub.gadget.app/api/v1/resources/bulk" \
  -H "Authorization: Bearer rhk_…" \
  -H "Content-Type: application/json" \
  -d '{ "resources": [ { "title": "Manual A", "format": "File", "link": "https://example.com/a.pdf" }, { "title": "Manual B", "format": "File", "link": "https://example.com/b.pdf" } ] }'

POST /resources/bulk/uploads

Start a staged File batch (max 25 names). Returns time-limited PUT URLs. Bytes go client → S3; do not invent file_url values. Allowed extensions match shop file types (pdf, jpg, png, zip, …). PUT must send the same Content-Type as content_type (or the type inferred from file_name).

curl -sS -X POST "https://resource-hub.gadget.app/api/v1/resources/bulk/uploads" \
  -H "Authorization: Bearer rhk_…" \
  -H "Content-Type: application/json" \
  -d '{ "files": [ { "file_name": "manual-a.pdf" }, { "file_name": "manual-b.pdf", "content_type": "application/pdf" } ] }'
{
  "data": {
    "job_id": "123",
    "expires_in": 1800,
    "poll_url": "/api/v1/jobs/123",
    "files": [
      {
        "upload_id": "upl-1",
        "file_name": "manual-a.pdf",
        "upload_url": "https://s3.amazonaws.com/…",
        "file_url": "https://s3.us-east-2.amazonaws.com/bucket/shop/bulk-import/123/upl-1_manual-a.pdf"
      }
    ]
  }
}

PUT each file, then complete (copy upload_id from the session; do not reuse another session’s URLs):

curl -sS -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @./manual-a.pdf

curl -sS -X POST "https://resource-hub.gadget.app/api/v1/resources/bulk" \
  -H "Authorization: Bearer rhk_…" \
  -H "Content-Type: application/json" \
  -d '{
  "job_id": "123",
  "resources": [
    { "title": "Manual A", "format": "File", "upload_id": "upl-1", "resource_type": "Manual" },
    { "title": "Spec sheet", "format": "Link", "link": "https://example.com/spec" }
  ]
}'

DELETE /resources/:id

Delete one resource owned by this shop. Returns 404 if the id is missing, already deleted, or belongs to another shop.

curl -sS -X DELETE "https://resource-hub.gadget.app/api/v1/resources/123456" \
  -H "Authorization: Bearer rhk_…"

POST /resources/delete

Delete by ids (max 50). Ids from other shops appear in not_found and are not changed.

curl -sS -X POST "https://resource-hub.gadget.app/api/v1/resources/delete" \
  -H "Authorization: Bearer rhk_…" \
  -H "Content-Type: application/json" \
  -d '{ "ids": ["123456", "123457"] }'
{ "data": { "deleted": ["123456"], "not_found": ["123457"] } }

Write fields (create and update)

Same catalog as CSV Import. POST /resources, PATCH /resources/:id, and each item in POST /resources/bulk accept these keys. PATCH is sparse: omit a key or send it empty and it is not written. format is applied on create only; PATCH never changes format. File vs Link/Video chooses stored copy vs URL. Do not send a separate external-link flag.

Lists: JSON array or pipe-separated string. Multipart: categories=["Manuals"]. Custom field CSV headers such as cf.lot_batch_number map to custom_fields.lot_batch_number. Keys must already exist on the resource type. Types, formats, languages, and SKUs must already exist for the shop.

FieldTypeCreatePATCHDescription
titlestringRequiredOptionalResource name.
statusstringOptionalOptionalactive (published) or draft (hidden). Default on create: active.
formatstringOptionalIgnoredFile, Video, Link, or Article. File stores a copy; Link and Video use link.
linkurlOptionalOptionalPublic URL. Alias: external_link. File: http(s) file URL (not YouTube/Vimeo). Video: YouTube or Vimeo. Link: any http(s) page. Or send multipart file on a File resource.
fileuploadOptionalFile onlyLocal file (multipart). Sets format to File. Max 50 MB. Bulk local files use POST /resources/bulk/uploads + upload_id.
languagestringOptionalOptionalLanguage code (en, es, pt_br), name, or id.
resource_typestringOptionalOptionalType name or id already enabled for the shop (e.g. Manual, Certificate of Analysis).
descriptionstringOptionalOptionalBrief overview. Alias: short_description.
page_contentstringOptionalOptionalFull page HTML. Alias: full_page_content.
categoriesstring[]OptionalReplace setTop-level category names. Pipe or array. Max 20. Sending values replaces the full set.
tagsstring[]OptionalReplace setTag names. Pipe or array. Max 20. Sending values replaces the full set.
product_skusstring[]OptionalReplace setShopify variant SKUs. Alias: skus. Max 50.
product_handlesstring[]OptionalReplace setShopify product handles when there is no SKU. Max 50.
featuredbooleanOptionalOptionalShow at the top of the library.
document_numberstringOptionalOptionalInternal document number (e.g. WB-1012).
handlestringOptionalOptionalLibrary URL slug (e.g. installation-manual-v2). Generated from title if omitted.
meta_titlestringOptionalOptionalSEO title.
meta_descriptionstringOptionalOptionalSEO description.
no_indexbooleanOptionalOptionalAsk search engines not to index the resource page.
image_urlurlOptionalOptionalCustom thumbnail URL.
image_sourcestringOptionalOptionalautogenerated, custom, or product.
custom_fieldsobjectOptionalUpsert keysShop custom fields, e.g. {"lot_batch_number":"LOT-44771"}. Only keys sent are upserted.

Not accepted: shop id, and ResourceHub external id. Identify a PATCH target with the id in the URL only. Do not send Shopify product ids; use SKUs or handles.

Response schema

{
  "data": [
    {
      "id": "123456",
      "title": "Model 5308 User Manual",
      "description": "Installation and operation guide",
      "resource_type": "Manual",
      "format": "File",
      "language": "en",
      "status": "active",
      "visibility": "public",
      "resource_page_url": "https://store.example.com/apps/resources/model-5308-user-manual",
      "file_url": "https://resourcehubfiles-dev.s3.us-east-2.amazonaws.com/100015-odegitest/files/Test_45MB.pdf",
      "categories": ["Manuals"],
      "tags": ["5308"],
      "skus": ["ABC-100"],
      "created_at": "2026-08-01T12:00:00.000Z",
      "updated_at": "2026-09-16T18:04:00.000Z"
    }
  ],
  "page_info": { "has_next_page": false, "end_cursor": null }
}

page_content is on GET /resources/:id and on create/update responses. GET omits file_extension and file_size; those fields remain on create/update. file_url is the S3 object URL without query parameters. Poll updated_since to detect replacements. Webhooks are not in this version.

Errors

HTTPcodeWhen
400invalid_request / invalid_updated_sinceMissing filter, bad body, or unknown type/format/sku
401unauthorizedMissing, invalid, or revoked key
403forbiddenKey is valid but missing the required scope
404not_foundUnknown id, not public (GET), or not owned by this shop (PATCH/DELETE)
409conflictBulk upload session already completed
413payload_too_largemultipart file over 50 MB
429rate_limitedToo many requests
500internal_errorUnexpected failure
{ "error": { "code": "unauthorized", "message": "…" } }