{
  "info": {
    "name": "Todify Developer API",
    "description": "# Todify Developer API\n\nOfficial API for **Custom Store** integrations. Connect your own e-commerce website or app directly to Todify print-on-demand fulfillment.\n\n---\n\n## 🔑 Authentication\n\nAll endpoints require a **Bearer token**:\n\n```\nAuthorization: Bearer YOUR_API_TOKEN\n```\n\nGet your token: **Stores → Connect Store → Developer API → Generate Token**\n\n---\n\n## 🌐 Base URL\n\n```\n{{base_url}}/api/v1\n```\n\n---\n\n## ⚡ Rate Limiting\n\n**60 requests per minute** per token.\nExceeded → HTTP `429 Too Many Requests` + `Retry-After` header.\n\n---\n\n## ⚡ Integration Workflow\n\n```\n1. [Setup]      GET  /api/v1/products/templates   → Import your templates\n2. [Push]       Webhook → template.created        → Sync new templates\n3. [Order]      POST /api/v1/orders              → Send customer order to Todify\n4. [Track]      Webhook → order.status_changed    → Update your customers\n5. [Stock]      GET  /api/v1/stock                → Sync sold-out sizes/colors\n                Webhook → stock.changed           → Keep them up to date\n```\n\n---\n\n## 📦 Webhook Security\n\nEvery webhook request Todify sends includes:\n\n```http\nX-Todify-Signature:   sha256=<hmac_sha256_of_body>\nX-Todify-Event:       order.status_changed\nX-Todify-Timestamp:   2024-04-01T10:30:00.000Z\nX-Todify-Delivery-Id: uuid-for-idempotency\nUser-Agent:           Todify-Webhook/1.0\n```\n\nVerify using your webhook `secret`:\n```php\n$expected = 'sha256=' . hash_hmac('sha256', file_get_contents('php://input'), $secret);\nif (!hash_equals($expected, $_SERVER['HTTP_X_TODIFY_SIGNATURE'])) abort(401);\n```\n\n### 📋 Webhook Event Payloads\n\n#### `template.created`\nFired when seller pushes a template to your store.\n```json\n{\n  \"event\": \"template.created\",\n  \"timestamp\": \"2024-04-01T10:00:00.000Z\",\n  \"data\": {\n    \"product_id\": \"01HTZ...\",\n    \"name\": \"Custom T-Shirt\",\n    \"price\": 125.00,\n    \"currency\": \"MAD\",\n    \"images\": [\"https://cdn.todify.ma/preview.jpg\"],\n    \"options\": [\"Size\", \"Color\"],\n    \"variants\": [{\"size\": \"M\", \"color\": \"White\"}]\n  }\n}\n```\n\n#### `template.updated`\nFired when seller updates a pushed template.\n```json\n{\n  \"event\": \"template.updated\",\n  \"timestamp\": \"2024-04-01T11:00:00.000Z\",\n  \"data\": {\n    \"product_id\": \"01HTZ...\",\n    \"name\": \"Custom T-Shirt\",\n    \"price\": 130.00,\n    \"currency\": \"MAD\",\n    \"images\": [\"https://cdn.todify.ma/preview.jpg\"],\n    \"options\": [\"Size\", \"Color\"],\n    \"variants\": [{\"size\": \"M\", \"color\": \"White\"}]\n  }\n}\n```\n\n#### `template.deleted`\nFired when seller removes a template.\n```json\n{\n  \"event\": \"template.deleted\",\n  \"timestamp\": \"2024-04-01T11:30:00.000Z\",\n  \"data\": {\n    \"product_id\": \"01HTZ...\"\n  }\n}\n```\n\n#### `order.created`\n```json\n{\n  \"event\": \"order.created\",\n  \"timestamp\": \"2024-04-01T12:00:00.000Z\",\n  \"data\": {\n    \"order_id\": \"01HTZ...\",\n    \"reference_code\": \"ORD-12345\",\n    \"external_id\": \"your-order-id\",\n    \"status\": \"store_draft_order\",\n    \"total_amount\": 250.00,\n    \"currency\": \"MAD\",\n    \"items\": [{\"product_id\": \"01HTZ...\", \"name\": \"T-Shirt\", \"quantity\": 2, \"price\": 125.00}]\n  }\n}\n```\n\n#### `order.status_changed`\n```json\n{\n  \"event\": \"order.status_changed\",\n  \"timestamp\": \"2024-04-01T15:00:00.000Z\",\n  \"data\": {\n    \"order_id\": \"01HTZ...\",\n    \"status\": \"shipped\",\n    \"previous_status\": \"in_production\",\n    \"tracking_number\": \"MA123456789\",\n    \"shipping_carrier\": \"Amana\",\n    \"items\": [{\"sku\": \"TSHIRT-M\", \"quantity\": 2, \"status\": \"shipped\"}]\n  }\n}\n```\n\n#### `stock.changed`\nFired when a size/color of your templates goes `in_stock`, `low_stock` or `out_of_stock`.\n```json\n{\n  \"event\": \"stock.changed\",\n  \"timestamp\": \"2024-04-01T16:00:00.000Z\",\n  \"data\": {\n    \"items\": [\n      {\"product_id\": \"01HTZ...\", \"sku\": \"CLO-EHK-1\", \"variant\": {\"size\": \"XL\", \"color\": \"Black\"}, \"stock_status\": \"out_of_stock\"}\n    ]\n  }\n}\n```\n\n---\n\n## 📊 Order Status Reference\n\n| Status | Meaning |\n|--------|---------|\n| `store_draft_order` | Received, awaiting seller confirmation |\n| `pending` | Confirmed, awaiting production |\n| `in_production` | Being manufactured |\n| `shipped` | Dispatched — tracking number available |\n| `delivered` | Successfully delivered |\n| `cancelled` | Order cancelled |\n| `returned` | Package returned |\n\n---\n\n## 🔄 Webhook Retry Policy\n\n| Attempt | Delay |\n|---------|-------|\n| 1st retry | 30 seconds |\n| 2nd retry | 60 seconds |\n| 3rd retry | 120 seconds |\n\nRespond within **15 seconds** and always return `2xx`. Use `X-Todify-Delivery-Id` to deduplicate retries.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "_postman_id": "todify-developer-api-v1"
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://todify.ma/api/v1",
      "type": "string",
      "description": "Base URL of the Todify API"
    },
    {
      "key": "api_token",
      "value": "YOUR_API_TOKEN_HERE",
      "type": "string",
      "description": "Your Todify API token (generate from dashboard)"
    }
  ],
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{api_token}}",
        "type": "string"
      }
    ]
  },
  "item": [
    {
      "name": "Store",
      "item": [
        {
          "name": "Get Store Info",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/store",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "store"
              ]
            },
            "description": "Returns your seller account info, balance, and connected developer store details."
          },
          "response": [
            {
              "name": "200 OK",
              "status": "OK",
              "code": 200,
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"seller\": {\n      \"email\": \"seller@example.com\",\n      \"balance\": 450.00,\n      \"currency\": \"MAD\"\n    },\n    \"store\": {\n      \"id\": \"01jq...\",\n      \"name\": \"My Online Store\",\n      \"platform\": \"developer\",\n      \"currency\": \"MAD\",\n      \"webhook_count\": 1,\n      \"created_at\": \"2026-04-01T00:00:00.000000Z\"\n    }\n  }\n}"
            }
          ],
          "description": "Returns your Todify seller account info, balance, and Developer API store details.\n\n**Use this to:**\n- Verify your API token is working correctly\n- Check your current account balance\n- Confirm your Developer API store is connected"
        }
      ],
      "description": "Retrieve your seller account info, current balance, and Developer API store details.\n\nThis is the best endpoint to use as a **connection health check** — if it returns 200, your token is valid and your store is connected."
    },
    {
      "name": "Product Templates",
      "item": [
        {
          "name": "List Templates",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/products/templates?page=1",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "products",
                "templates"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1",
                  "description": "Page number (default: 1, 20 items per page)"
                }
              ]
            },
            "description": "Returns a paginated list of your approved templates. Use the `id` field in the orders endpoint."
          },
          "response": [
            {
              "name": "200 OK",
              "status": "OK",
              "code": 200,
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"template-uuid-here\",\n      \"name\": \"Custom T-Shirt\",\n      \"description\": \"Premium quality t-shirt\",\n      \"sku\": \"TSHIRT-001\",\n      \"thumbnail\": \"https://todify.ma/storage/preview.jpg\",\n      \"images\": []\n    }\n  ],\n  \"meta\": {\n    \"current_page\": 1,\n    \"last_page\": 3,\n    \"per_page\": 20,\n    \"total\": 58\n  }\n}"
            }
          ],
          "description": "List all product templates belonging to your seller account.\n\n## Response Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `id` | string | Unique product ID to save as `product_id` and use in `POST /orders` |\n| `name` | string | Template name set by the seller |\n| `description` | string\\|null | HTML description |\n| `price` | null | Price is set by the seller per store — not stored in Todify |\n| `sku` | string | Auto-generated SKU code |\n| `thumbnail` | string | Small 500×500 preview image URL (for listings/cards) |\n| `images` | array | Full-size original images per surface (for product pages) |\n\n## Pagination\n\nResults are paginated at **20 per page**. Use `?page=2` etc.\n\n```json\n{\n  \"success\": true,\n  \"data\": [...],\n  \"meta\": {\n    \"current_page\": 1,\n    \"last_page\": 3,\n    \"per_page\": 20,\n    \"total\": 54\n  }\n}\n```\n\n## Syncing Products\n\nRegister a `template.created` webhook to be notified instantly when a seller pushes a product to your store. The webhook `data.product_id` is the same value as this endpoint's `id` field."
        },
        {
          "name": "Get Template Details",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/products/templates/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "products",
                "templates",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "template-uuid-here",
                  "description": "Template ID from List Templates"
                }
              ]
            },
            "description": "Get full details of a single template, including all variant combinations (size × color) and their stock status."
          },
          "response": [
            {
              "name": "200 OK",
              "status": "OK",
              "code": 200,
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"template-uuid-here\",\n    \"name\": \"Custom T-Shirt\",\n    \"attributes\": {\n      \"size\": {\n        \"S\": \"S\",\n        \"M\": \"M\"\n      },\n      \"color\": {\n        \"Black\": \"#121212\",\n        \"White\": \"#f7f7f7\"\n      }\n    },\n    \"stock_status\": \"in_stock\",\n    \"variants\": [\n      {\n        \"variant\": {\n          \"size\": \"S\",\n          \"color\": \"Black\"\n        },\n        \"stock_status\": \"in_stock\"\n      },\n      {\n        \"variant\": {\n          \"size\": \"S\",\n          \"color\": \"White\"\n        },\n        \"stock_status\": \"low_stock\"\n      },\n      {\n        \"variant\": {\n          \"size\": \"M\",\n          \"color\": \"Black\"\n        },\n        \"stock_status\": \"out_of_stock\"\n      },\n      {\n        \"variant\": {\n          \"size\": \"M\",\n          \"color\": \"White\"\n        },\n        \"stock_status\": \"in_stock\"\n      }\n    ]\n  }\n}"
            }
          ],
          "description": "Get full details of a single product template including all variant combinations.\n\n## Path Parameter\n\n| Parameter | Description |\n|-----------|-------------|\n| `id` | The product template UUID from `GET /api/v1/products/templates` |\n\n## Additional Fields (vs list endpoint)\n\n| Field | Description |\n|-------|-------------|\n| `attributes` | Raw attribute map e.g. `{\"size\": {\"S\": \"S\", \"M\": \"M\"}, \"color\": {\"White\": \"White\"}}` |\n| `stock_status` | Whole template: `out_of_stock` when no size/color is left |\n| `variants` | All possible variant combinations generated from attributes, each with its `stock_status` |\n\n## Example Response\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"01HTZ...\",\n    \"name\": \"Classic T-Shirt\",\n    \"sku\": \"CLO-EHK-1\",\n    \"thumbnail\": \"https://...thumbnail.png\",\n    \"images\": [\"https://...full-size.jpeg\"],\n    \"attributes\": {\n      \"size\": {\"S\": \"S\", \"M\": \"M\", \"L\": \"L\"},\n      \"color\": {\"White\": \"#f7f7f7\", \"Black\": \"#121212\"}\n    },\n    \"stock_status\": \"in_stock\",\n    \"variants\": [\n      {\"variant\": {\"size\": \"S\", \"color\": \"White\"}, \"stock_status\": \"in_stock\"},\n      {\"variant\": {\"size\": \"S\", \"color\": \"Black\"}, \"stock_status\": \"low_stock\"},\n      {\"variant\": {\"size\": \"M\", \"color\": \"White\"}, \"stock_status\": \"out_of_stock\"}\n    ]\n  }\n}\n```\n\n## Stock Status\n\n| Status | Meaning |\n|--------|---------|\n| `in_stock` | Available |\n| `low_stock` | Only a few left: it can sell out soon |\n| `out_of_stock` | Not available: hide it or mark it sold out |\n\nTodify never shares how many pieces are left.\n\n## Using Variant in an Order\n\nPass the `variant` object directly into `items[].variant` when submitting an order:\n\n```json\n{\n  \"items\": [{\n    \"product_id\": \"01HTZ...\",\n    \"variant\": {\"size\": \"M\", \"color\": \"White\"},\n    \"quantity\": 1,\n    \"price\": 125.00\n  }]\n}\n```"
        },
        {
          "name": "Get Stock",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/stock?page=1",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "stock"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1",
                  "description": "Page number (default: 1, 50 templates per page)"
                }
              ]
            },
            "description": "Stock of all your templates, per size/color: in_stock, low_stock or out_of_stock."
          },
          "response": [
            {
              "name": "200 OK",
              "status": "OK",
              "code": 200,
              "body": "{\n  \"success\": true,\n  \"data\": [\n    {\n      \"id\": \"template-uuid-here\",\n      \"name\": \"Custom T-Shirt\",\n      \"sku\": \"CLO-EHK-1\",\n      \"stock_status\": \"in_stock\",\n      \"variants\": [\n        {\n          \"variant\": {\n            \"size\": \"M\",\n            \"color\": \"Black\"\n          },\n          \"stock_status\": \"low_stock\"\n        },\n        {\n          \"variant\": {\n            \"size\": \"XL\",\n            \"color\": \"Black\"\n          },\n          \"stock_status\": \"out_of_stock\"\n        }\n      ]\n    }\n  ],\n  \"meta\": {\n    \"current_page\": 1,\n    \"last_page\": 1,\n    \"per_page\": 50,\n    \"total\": 1\n  }\n}"
            }
          ],
          "description": "Stock of every template in your account, for each size/color. Use it to mark sold-out sizes/colors on your store, then keep them up to date with the `stock.changed` webhook.\n\n## Response Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `id` | string | Template ID (the `product_id` you use in `POST /orders`) |\n| `name` | string | Template name |\n| `sku` | string | SKU code |\n| `stock_status` | string | Whole template: `out_of_stock` when no size/color is left |\n| `variants` | array | Each size/color (`variant`, same values as in orders) with its `stock_status` |\n\n## Stock Status\n\n| Status | Meaning |\n|--------|---------|\n| `in_stock` | Available |\n| `low_stock` | Only a few left: it can sell out soon |\n| `out_of_stock` | Not available: hide it or mark it sold out |\n\nTodify never shares how many pieces are left."
        }
      ],
      "description": "Browse your templates available for print-on-demand fulfillment.\n\nUse the `id` from each template as the `product_id` when submitting orders.\n\n**When to use this:**\n- On first setup: import all your templates into your custom store\n- Periodically: sync any new templates you've added in Todify\n- Alternative: listen for `template.created` / `template.updated` webhooks to get updates automatically"
    },
    {
      "name": "Orders",
      "item": [
        {
          "name": "Submit New Order",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"external_id\": \"YOUR_ORDER_123\",\n  \"payment_method\": \"cod\",\n  \"note\": \"Please handle with care\",\n  \"shipping\": {\n    \"firstname\": \"Ali\",\n    \"lastname\": \"Ourrami\",\n    \"phone\": \"0612345678\",\n    \"address\": \"123 Rue Hassan II\",\n    \"city\": \"Casablanca\",\n    \"state\": null,\n    \"country\": \"MA\"\n  },\n  \"items\": [\n    {\n      \"product_id\": \"template-uuid-here\",\n      \"variant\": {\n        \"Size\": \"M\",\n        \"Color\": \"Black\"\n      },\n      \"quantity\": 1,\n      \"price\": 150.00\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/orders",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "orders"
              ]
            },
            "description": "Submit a customer order to Todify for fulfillment.\n\nThe order is created with `store_draft_order` status — it will appear in your Todify dashboard pending your confirmation.\n\n**Required fields:**\n- `shipping.firstname`, `shipping.phone`, `shipping.address`, `shipping.city`\n- `items[].product_id` — must be a valid template ID from `GET /products/templates`\n- `items[].quantity` — number of items\n- `items[].price` — price per item in your store currency\n\n**Optional:**\n- `external_id` — your internal order ID for cross-reference\n- `variant` — size/color combination\n- `note` — delivery note"
          },
          "response": [
            {
              "name": "201 Created",
              "status": "Created",
              "code": 201,
              "body": "{\n  \"success\": true,\n  \"message\": \"Order created successfully. It will appear in your Todify dashboard as a draft pending confirmation.\",\n  \"data\": {\n    \"id\": \"01jq...order-id\",\n    \"reference_code\": \"TOD-123456\",\n    \"external_id\": \"YOUR_ORDER_123\",\n    \"status\": \"store_draft_order\",\n    \"total_amount\": 150.00,\n    \"currency\": \"MAD\",\n    \"created_at\": \"2026-04-01T00:15:00.000000Z\"\n  }\n}"
            },
            {
              "name": "422 Validation Error",
              "status": "Unprocessable Entity",
              "code": 422,
              "body": "{\n  \"message\": \"The shipping.firstname field is required.\",\n  \"errors\": {\n    \"shipping.firstname\": [\"The shipping.firstname field is required.\"]\n  }\n}"
            }
          ],
          "description": "Submit a new customer order to Todify for print-on-demand fulfillment.\n\n## Limits\n\n| Field | Constraint |\n|-------|-----------|\n| `items` array | Max **50 line items** |\n| `items.*.quantity` | Max **100 units** per line |\n| `items.*.price` | Must be **> 0** |\n| `note` | Max **500 characters** |\n\n## Order Flow\n\n1. Order created as `store_draft_order`\n2. Seller confirms in Todify dashboard → `pending`\n3. Production begins → `in_production`\n4. Ships → `shipped` (tracking number available)\n\n## Getting Product IDs\n\nUse `data.product_id` from the `template.created` webhook, or call `GET /api/v1/products/templates` and use the `id` field as `items[].product_id`.\n\n## Tracking the Order\n\n- Save the `id` from the response\n- Poll: `GET /api/v1/orders/{id}`\n- Real-time: Register a webhook for `order.status_changed`"
        },
        {
          "name": "List Orders",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/orders?page=1",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "orders"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1"
                }
              ]
            },
            "description": "List all orders submitted through your Developer API, with their current fulfillment status."
          },
          "description": "Returns a paginated list of all orders submitted through your Developer API.\n\nOrders are sorted newest first (20 per page).\n\n**Use this to:**\n- Reconcile your orders between your store and Todify\n- Check status without webhooks (polling fallback)"
        },
        {
          "name": "Get Order Status",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/orders/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "orders",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "order-id-here",
                  "description": "Order ID returned from Submit Order"
                }
              ]
            },
            "description": "Get detailed status of a specific order including shipping info and line items."
          },
          "response": [
            {
              "name": "200 OK",
              "status": "OK",
              "code": 200,
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"01jq...order-id\",\n    \"reference_code\": \"TOD-123456\",\n    \"external_id\": \"YOUR_ORDER_123\",\n    \"status\": \"shipped\",\n    \"total_amount\": 150.00,\n    \"currency\": \"MAD\",\n    \"shipping\": {\n      \"name\": \"Ali Ourrami\",\n      \"city\": \"Casablanca\",\n      \"address\": \"123 Rue Hassan II\",\n      \"phone\": \"0612345678\"\n    },\n    \"items\": [\n      {\n        \"name\": \"Custom T-Shirt\",\n        \"variant_title\": \"Size: M, Color: Black\",\n        \"quantity\": 1,\n        \"price\": 150.00,\n        \"status\": \"shipped\"\n      }\n    ]\n  }\n}"
            }
          ],
          "description": "Get full details and current status of a specific order.\n\nPass the Todify `order_id` (returned when you submitted the order) as the URL parameter.\n\n**Response includes:**\n- Current status\n- Tracking number (when shipped)\n- Shipping address details\n- All order items with individual statuses\n\n> 💡 Tip: For real-time updates, use `order.status_changed` webhooks instead of polling this endpoint."
        }
      ],
      "description": "Submit customer orders from your store to Todify for print-on-demand fulfillment.\n\n**Order Lifecycle:**\n1. You call `POST /api/v1/orders` → Todify creates the order as `store_draft_order`\n2. Seller reviews and confirms in Todify dashboard → status changes to `pending`\n3. Production begins → status changes to `in_production`\n4. Order ships → status changes to `shipped` (tracking number available)\n5. Listen for `order.status_changed` webhooks at every step!"
    },
    {
      "name": "Webhooks",
      "item": [
        {
          "name": "Register Webhook",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              },
              {
                "key": "Accept",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://yourstore.com/api/todify-webhook?event=template.created\",\n  \"event\": \"template.created\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{base_url}}/webhooks",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "webhooks"
              ]
            },
            "description": "Register a webhook URL to receive real-time events.\n\nWhen an event fires, Todify will POST to your URL with an HMAC-SHA256 signature in the `X-Todify-Signature` header.\n\n## Supported Events\n\n| Event | When it fires |\n|-------|---------------|\n| `template.created` | Seller clicks **Push To Store** for your Developer API store |\n| `order.created` | Your store creates an order with `POST /api/v1/orders` |\n| `order.status_changed` | A Developer API order status changes in Todify |\n| `stock.changed` | A size/color of your templates goes `in_stock`, `low_stock` or `out_of_stock` |\n| `template.updated` | Reserved for future template update sync |\n| `template.deleted` | Reserved for future template delete sync |\n\n## Request Body\n\nRegister one event per webhook. To receive product pushes, register `template.created` before clicking **Push To Store** in Todify.\n\n```json\n{\n  \"url\": \"https://yourstore.com/api/todify-webhook?event=template.created\",\n  \"event\": \"template.created\"\n}\n```\n\nAvailable `event` values:\n\n```json\n[\n  \"template.created\",\n  \"order.created\",\n  \"order.status_changed\",\n  \"stock.changed\",\n  \"template.updated\",\n  \"template.deleted\"\n]\n```\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `url` | string | ✅ Yes | Your HTTPS endpoint URL. Localhost and private IPs are rejected. |\n| `event` | string | No | One value from the list above. Defaults to `order.status_changed` if omitted. |\n\n## Multiple Events\n\nIf the same receiver handles multiple events, register each event separately. Duplicate URLs are rejected, so either use different endpoint paths or add a query string per event:\n\n```json\n{ \"url\": \"https://yourstore.com/api/todify-webhook?event=template.created\", \"event\": \"template.created\" }\n{ \"url\": \"https://yourstore.com/api/todify-webhook?event=order.created\", \"event\": \"order.created\" }\n{ \"url\": \"https://yourstore.com/api/todify-webhook?event=order.status_changed\", \"event\": \"order.status_changed\" }\n{ \"url\": \"https://yourstore.com/api/todify-webhook?event=stock.changed\", \"event\": \"stock.changed\" }\n```\n\n> Save the `secret` returned in the response — it is only shown once.\n\n## Signature Verification\n\n**PHP:**\n```php\n$payload  = file_get_contents('php://input');\n$secret   = $_ENV['TODIFY_WEBHOOK_SECRET'];\n$header   = $_SERVER['HTTP_X_TODIFY_SIGNATURE'] ?? '';\n$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);\n\nif (!hash_equals($expected, $header)) {\n    http_response_code(401);\n    exit('Unauthorized');\n}\n$event = json_decode($payload, true);\n```\n\n**Node.js:**\n```javascript\nconst crypto = require('crypto');\napp.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {\n    const sig      = req.headers['x-todify-signature'];\n    const expected = 'sha256=' + crypto\n        .createHmac('sha256', process.env.TODIFY_WEBHOOK_SECRET)\n        .update(req.body).digest('hex');\n    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig)))\n        return res.status(401).send('Unauthorized');\n    const event = JSON.parse(req.body);\n    res.sendStatus(200);\n});\n```"
          },
          "response": [
            {
              "name": "201 Created",
              "status": "Created",
              "code": 201,
              "body": "{\n  \"success\": true,\n  \"message\": \"Webhook registered successfully.\",\n  \"data\": {\n    \"id\": 1,\n    \"url\": \"https://yourstore.com/api/todify-webhook?event=template.created\",\n    \"event\": \"template.created\",\n    \"secret\": \"abc123def456...\"\n  }\n}"
            }
          ],
          "description": "Register a URL to receive real-time events from Todify.\n\n## Requirements\n\n- `url` must use **HTTPS** (HTTP URLs are rejected with 422)\n- `url` cannot point to **localhost** or private IP ranges (SSRF protection)\n- Maximum **10 webhooks** per store\n- Exact duplicate URLs are rejected\n\n## Request Body\n\nRegister one event per webhook. To receive product pushes from the seller dashboard, register `template.created` before clicking **Push To Store**.\n\n```json\n{\n  \"url\": \"https://yourstore.com/api/todify-webhook?event=template.created\",\n  \"event\": \"template.created\"\n}\n```\n\nAvailable `event` values:\n\n```json\n[\n  \"template.created\",\n  \"order.created\",\n  \"order.status_changed\",\n  \"template.updated\",\n  \"template.deleted\"\n]\n```\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `url` | string | Yes | Your HTTPS endpoint URL, max 500 chars. |\n| `event` | string | No | One value from the event list. Defaults to `order.status_changed` if omitted. |\n\n## Multiple Events\n\nIf the same receiver handles multiple events, register each event separately. Use different endpoint paths or add query strings so each URL is unique:\n\n```json\n{ \"url\": \"https://yourstore.com/api/todify-webhook?event=template.created\", \"event\": \"template.created\" }\n{ \"url\": \"https://yourstore.com/api/todify-webhook?event=order.created\", \"event\": \"order.created\" }\n{ \"url\": \"https://yourstore.com/api/todify-webhook?event=order.status_changed\", \"event\": \"order.status_changed\" }\n```\n\n## Response (201 Created)\n\n```json\n{\n  \"success\": true,\n  \"message\": \"Webhook registered successfully.\",\n  \"data\": {\n    \"id\": 1,\n    \"url\": \"https://yourstore.com/api/todify-webhook?event=template.created\",\n    \"event\": \"template.created\",\n    \"secret\": \"a1b2c3d4...64-hex-chars\"\n  }\n}\n```\n\n> The `secret` is shown only once. Store it immediately in your `.env` or secrets manager. It is never returned again — if lost, delete and re-register.\n\n## Verifying the Signature (PHP)\n\n```php\n$payload  = file_get_contents('php://input');\n$secret   = $_ENV['TODIFY_WEBHOOK_SECRET'];\n$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);\n$received = $_SERVER['HTTP_X_TODIFY_SIGNATURE'] ?? '';\n\nif (!hash_equals($expected, $received)) {\n    http_response_code(401);\n    exit('Unauthorized');\n}\n\n$event = json_decode($payload, true);\n// Process $event...\n```\n\n## Verifying the Signature (Node.js)\n\n```javascript\nconst crypto = require('crypto');\n\napp.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {\n    const sig      = req.headers['x-todify-signature'];\n    const expected = 'sha256=' + crypto\n        .createHmac('sha256', process.env.TODIFY_WEBHOOK_SECRET)\n        .update(req.body)\n        .digest('hex');\n\n    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {\n        return res.status(401).send('Unauthorized');\n    }\n\n    const event = JSON.parse(req.body);\n    res.sendStatus(200); // Always respond quickly\n});\n```\n\n## Request Headers Sent by Todify\n\n| Header | Example | Description |\n|--------|---------|-------------|\n| `X-Todify-Signature` | `sha256=abc123...` | HMAC-SHA256 of the raw JSON body |\n| `X-Todify-Event` | `template.created` | Which event fired |\n| `X-Todify-Timestamp` | `2024-04-01T12:00:00.000Z` | When the event was triggered |\n| `X-Todify-Delivery-Id` | `550e8400-e29b-41d4-a716` | Unique UUID — use for idempotency on retries |\n| `User-Agent` | `Todify-Webhook/1.0` | Always Todify-Webhook/1.0 |\n\n## Retry Policy\n\nIf your endpoint returns a non-2xx response or times out (15s), Todify retries:\n- 1st retry after **30 seconds**\n- 2nd retry after **60 seconds**  \n- 3rd retry after **120 seconds**\n\nUse `X-Todify-Delivery-Id` to deduplicate retries on your end."
        },
        {
          "name": "List Webhooks",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Accept",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/webhooks",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "webhooks"
              ]
            }
          },
          "description": "List all registered webhooks for your Developer API store.\n\n> 🔒 **Security note**: The `secret` is **never returned** after initial creation. If lost, delete the webhook and re-register.\n\n## Response Fields\n\n| Field | Description |\n|-------|-------------|\n| `id` | Use this to delete the webhook |\n| `url` | Your registered HTTPS endpoint |\n| `event` | Which event this webhook subscribes to |\n| `is_active` | Whether Todify will send events to this URL |\n| `created_at` | ISO8601 timestamp of registration |"
        },
        {
          "name": "Delete Webhook",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "Accept",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{base_url}}/webhooks/:id",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "webhooks",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "1"
                }
              ]
            }
          },
          "description": "Remove a registered webhook by ID.\n\nAfter deletion Todify stops sending events to that URL immediately.\n\nUse `GET /api/v1/webhooks` to find the webhook `id`."
        }
      ],
      "description": "Register HTTPS endpoints on your server to receive real-time Todify events.\n\n## Supported Events\n\n| Event | When it fires |\n|-------|---------------|\n| `template.created` | Seller clicks **Push To Store** for your Developer API store |\n| `order.created` | Your store creates an order with `POST /api/v1/orders` |\n| `order.status_changed` | A Developer API order status changes in Todify |\n| `template.updated` | Reserved for future template update sync |\n| `template.deleted` | Reserved for future template delete sync |\n\n## Event Setup\n\nRegister one webhook per event. For product push, register `template.created` first:\n\n```json\n{\n  \"url\": \"https://yourstore.com/api/todify-webhook?event=template.created\",\n  \"event\": \"template.created\"\n}\n```\n\nAvailable `event` values:\n\n```json\n[\n  \"template.created\",\n  \"order.created\",\n  \"order.status_changed\",\n  \"template.updated\",\n  \"template.deleted\"\n]\n```\n\nDuplicate URLs are rejected. If the same endpoint handles multiple events, use different paths or query strings:\n\n```json\n{ \"url\": \"https://yourstore.com/api/todify-webhook?event=template.created\", \"event\": \"template.created\" }\n{ \"url\": \"https://yourstore.com/api/todify-webhook?event=order.created\", \"event\": \"order.created\" }\n{ \"url\": \"https://yourstore.com/api/todify-webhook?event=order.status_changed\", \"event\": \"order.status_changed\" }\n```\n\n## Security Requirements\n\n- HTTPS only — HTTP URLs are rejected\n- Max 10 webhooks per store\n- No exact duplicate URLs\n- Secret shown once — save it immediately after registration\n\n## Setup Steps\n\n1. Register a URL using `POST /api/v1/webhooks`\n2. Save the `secret` from the response — you will never see it again\n3. Verify every incoming request using the `X-Todify-Signature` header\n4. Return `200 OK` within 15 seconds"
    }
  ]
}
