ResourceHub API V1
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.
| Scope | Methods |
|---|---|
resources:read | GET /resources, GET /resources/:id, GET /files/:id |
resources:write | POST /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
| Control | Value |
|---|---|
| List / get | 60 requests / minute / key |
| File URL | 20 requests / minute / key |
| Create / update / delete | 20 requests / minute / key |
| Bulk create | 10 requests / minute / key, max 25 items |
| Bulk file uploads | Presigned PUT, 30 minutes; complete with the same job_id |
| Bulk delete | Max 50 ids |
| Direct file upload | 50 MB on POST /resources (multipart file) |
| Page size | Default 25, maximum 50 |
| Over limit | HTTP 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.
| Field | Type | Create | PATCH | Description |
|---|---|---|---|---|
title | string | Required | Optional | Resource name. |
status | string | Optional | Optional | active (published) or draft (hidden). Default on create: active. |
format | string | Optional | Ignored | File, Video, Link, or Article. File stores a copy; Link and Video use link. |
link | url | Optional | Optional | Public 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. |
file | upload | Optional | File only | Local file (multipart). Sets format to File. Max 50 MB. Bulk local files use POST /resources/bulk/uploads + upload_id. |
language | string | Optional | Optional | Language code (en, es, pt_br), name, or id. |
resource_type | string | Optional | Optional | Type name or id already enabled for the shop (e.g. Manual, Certificate of Analysis). |
description | string | Optional | Optional | Brief overview. Alias: short_description. |
page_content | string | Optional | Optional | Full page HTML. Alias: full_page_content. |
categories | string[] | Optional | Replace set | Top-level category names. Pipe or array. Max 20. Sending values replaces the full set. |
tags | string[] | Optional | Replace set | Tag names. Pipe or array. Max 20. Sending values replaces the full set. |
product_skus | string[] | Optional | Replace set | Shopify variant SKUs. Alias: skus. Max 50. |
product_handles | string[] | Optional | Replace set | Shopify product handles when there is no SKU. Max 50. |
featured | boolean | Optional | Optional | Show at the top of the library. |
document_number | string | Optional | Optional | Internal document number (e.g. WB-1012). |
handle | string | Optional | Optional | Library URL slug (e.g. installation-manual-v2). Generated from title if omitted. |
meta_title | string | Optional | Optional | SEO title. |
meta_description | string | Optional | Optional | SEO description. |
no_index | boolean | Optional | Optional | Ask search engines not to index the resource page. |
image_url | url | Optional | Optional | Custom thumbnail URL. |
image_source | string | Optional | Optional | autogenerated, custom, or product. |
custom_fields | object | Optional | Upsert keys | Shop 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
| HTTP | code | When |
|---|---|---|
| 400 | invalid_request / invalid_updated_since | Missing filter, bad body, or unknown type/format/sku |
| 401 | unauthorized | Missing, invalid, or revoked key |
| 403 | forbidden | Key is valid but missing the required scope |
| 404 | not_found | Unknown id, not public (GET), or not owned by this shop (PATCH/DELETE) |
| 409 | conflict | Bulk upload session already completed |
| 413 | payload_too_large | multipart file over 50 MB |
| 429 | rate_limited | Too many requests |
| 500 | internal_error | Unexpected failure |
{ "error": { "code": "unauthorized", "message": "…" } }