{
  "info": {
    "name": "Onboardly Partner API",
    "description": "Integration test collection for the Onboardly Partner API.\n\nSETUP\n1. Set the `apiKey` collection variable to your onb_live_... key.\n2. Set `baseUrl` (default https://rca-onboardly.com).\n3. Run requests in order, or use the Collection Runner — request 01 saves\n   the candidate id that later requests use.\n\nSAFETY\nEvery request here uses start:false, so no WhatsApp is ever sent and no\ncost is incurred. The phone number is an Ofcom reserved test range.\nOnly set start:true against a handset you control.\n\nSee https://rca-onboardly.com/docs for the full, always-current reference.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "01 — Create candidate (start:false, SAFE)",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{idempotencyKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates"
          ]
        },
        "description": "Creates a candidate WITHOUT sending WhatsApp. Saves the id to {{candidateId}}.\n\nstart MUST be exactly true to send. Anything else sends nothing.\nUses an Ofcom reserved test number (+447700900321) that cannot reach a real person.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"first_name\": \"Postman\",\n  \"last_name\": \"TestCandidate\",\n  \"email\": \"postman.test@example.com\",\n  \"mobile_number\": \"+447700900321\",\n  \"role_type\": \"Healthcare Assistant\",\n  \"documents\": [\n    \"DBS\",\n    \"Right to Work\",\n    \"Occupational Health Clearance\"\n  ],\n  \"references\": [\n    {\n      \"organisation\": \"Acme Care Ltd\",\n      \"role\": \"Team Lead\",\n      \"startDate\": \"2023-01-01\",\n      \"endDate\": \"2025-06-30\"\n    }\n  ],\n  \"opt_in\": true,\n  \"opt_in_source\": \"ats_import\",\n  \"opt_in_at\": \"2026-09-01T09:00:00Z\",\n  \"docs_chase_attempts\": 3,\n  \"reference_chase_attempts\": 2,\n  \"start\": false\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "// Fresh idempotency key per run, so each run creates a new candidate.",
              "pm.collectionVariables.set('idempotencyKey', 'pm-' + Date.now());"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('201 Created', () => pm.response.to.have.status(201));",
              "const d = pm.response.json().data;",
              "pm.collectionVariables.set('candidateId', d.id);",
              "pm.collectionVariables.set('lastIdempotencyKey', pm.collectionVariables.get('idempotencyKey'));",
              "pm.test('documents seeded', () => pm.expect(d.documentsSeeded).to.be.above(0));",
              "console.log('candidateId =', d.id);"
            ]
          }
        }
      ]
    },
    {
      "name": "02 — Replay: same Idempotency-Key, DIFFERENT body",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Idempotency-Key",
            "value": "{{lastIdempotencyKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates"
          ]
        },
        "description": "Proves idempotency: returns the FIRST candidate's id, creates nothing new.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"first_name\": \"Totally\",\n  \"last_name\": \"Different\",\n  \"email\": \"other@example.com\",\n  \"mobile_number\": \"+447700900999\",\n  \"start\": false\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 replay', () => pm.response.to.have.status(200));",
              "const d = pm.response.json().data;",
              "pm.test('replayed flag', () => pm.expect(d.replayed).to.eql(true));",
              "pm.test('same id, no second candidate', () =>",
              "  pm.expect(d.id).to.eql(pm.collectionVariables.get('candidateId')));"
            ]
          }
        }
      ]
    },
    {
      "name": "03 — Auth: no API key (expect 401)",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "Idempotency-Key",
            "value": "no-auth-probe"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates"
          ]
        },
        "description": "Every auth failure returns an identical 401 — no oracle.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"first_name\": \"A\",\n  \"last_name\": \"B\",\n  \"email\": \"a@b.com\",\n  \"mobile_number\": \"+447700900321\"\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('401', () => pm.response.to.have.status(401));"
            ]
          }
        }
      ]
    },
    {
      "name": "04 — Get candidate result",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates/{{candidateId}}",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates",
            "{{candidateId}}"
          ]
        },
        "description": "Full result payload. Poll this until compliance_pack.ready is true.\n\nchase.docs_attempts_max shows which tier applied: your value, else the account default."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "const d = pm.response.json().data;",
              "console.log('chase:', JSON.stringify(d.chase));",
              "console.log('reference_requirements:', JSON.stringify(d.reference_requirements));",
              "pm.test('external_id echoes the Idempotency-Key', () =>",
              "  pm.expect(d.external_id).to.eql(pm.collectionVariables.get('lastIdempotencyKey')));"
            ]
          }
        }
      ]
    },
    {
      "name": "05 — Patch candidate (identity, notes)",
      "request": {
        "method": "PATCH",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates/{{candidateId}}",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates",
            "{{candidateId}}"
          ]
        },
        "description": "Closed allowlist: first_name, last_name, email, mobile_number, role_type, notes.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"role_type\": \"Registered Nurse\",\n  \"notes\": \"Updated from Postman\"\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));"
            ]
          }
        }
      ]
    },
    {
      "name": "06 — Patch outreach (per-candidate chase)",
      "request": {
        "method": "PATCH",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates/{{candidateId}}/outreach",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates",
            "{{candidateId}}",
            "outreach"
          ]
        },
        "description": "Send null to clear a field back to the account default.\nchase_frequency_override is 1-14 days; out of range returns 400.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"chase_attempts_override\": 4,\n  \"reference_chase_attempts_override\": 1,\n  \"chase_frequency_override\": 3,\n  \"chase_when_quiet\": true\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "console.log('updated:', JSON.stringify(pm.response.json().data.updatedFields));"
            ]
          }
        }
      ]
    },
    {
      "name": "07 — Add documents (any name accepted)",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates/{{candidateId}}/documents",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates",
            "{{candidateId}}",
            "documents"
          ]
        },
        "description": "Both body shapes work. Any document name is accepted, not just the catalogue.\n\nThe response's `added` list contains only names that were NEW. A document already on the candidate's checklist is skipped silently, so `added` can be shorter than what you sent — that is dedupe, not an error. If EVERY name is already present you get a 409.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"documents\": [\n    \"Passport\",\n    {\n      \"name\": \"Professional Indemnity Insurance\",\n      \"customInstructions\": \"Certificate showing cover to 2026\"\n    }\n  ]\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('2xx', () => pm.expect(pm.response.code).to.be.oneOf([200,201]));"
            ]
          }
        }
      ]
    },
    {
      "name": "08 — Add a reference",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates/{{candidateId}}/references",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates",
            "{{candidateId}}",
            "references"
          ]
        },
        "description": "References are collected by EMAILING the referee, never uploaded by the candidate. This call only creates the request row — it never sends the referee email itself; see request 12 (PATCH) for what triggers the send.\n\n`template_id` (optional) pins one of your own reference templates to this request — see request 14 (List reference templates) for how to get an id. `null` (shown here) or omitting the key both mean no pin, which falls back to your default template.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"referee_email\": \"referee@acme.example\",\n  \"referee_name\": \"Jane Referee\",\n  \"referee_organisation\": \"Acme Care Ltd\",\n  \"employment_start_date\": \"2023-01-01\",\n  \"employment_end_date\": \"2025-06-30\",\n  \"template_id\": null\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('2xx', () => pm.expect(pm.response.code).to.be.oneOf([200,201]));",
              "const d = pm.response.json().data;",
              "pm.collectionVariables.set('referenceId', d.id);",
              "console.log('referenceId =', d.id);"
            ]
          }
        }
      ]
    },
    {
      "name": "09 — Get compliance pack (409 until complete)",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Accept",
            "value": "application/zip"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates/{{candidateId}}/pack",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates",
            "{{candidateId}}",
            "pack"
          ]
        },
        "description": "409 with a blockers list until every document is accepted. 200 returns the ZIP. Omit Accept (or send application/pdf) for the standalone PDF summary instead — that is the default."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('409 while incomplete, 200 when ready', () =>",
              "  pm.expect(pm.response.code).to.be.oneOf([200,409]));",
              "if (pm.response.code === 409) {",
              "  console.log('blockers:', JSON.stringify(pm.response.json().meta.blockers));",
              "}"
            ]
          }
        }
      ]
    },
    {
      "name": "10 — Tenancy: another account's candidate (expect 404)",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates/00000000-0000-4000-8000-000000000000",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates",
            "00000000-0000-4000-8000-000000000000"
          ]
        },
        "description": "Unknown and cross-tenant ids return an IDENTICAL 404 — never 403, so the endpoint cannot be used to discover whether an id exists."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('404', () => pm.response.to.have.status(404));"
            ]
          }
        }
      ]
    },
    {
      "name": "11 — Get document content",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates/{{candidateId}}/documents/{{documentId}}/content",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates",
            "{{candidateId}}",
            "documents",
            "{{documentId}}",
            "content"
          ]
        },
        "description": "Streams the bytes of the slot's LATEST submission — whatever its status (pending/needs_review/accepted/rejected), not only an accepted one. Take {{documentId}} from a document's content_url on request 04's response. 404 only when nothing has ever been submitted for that slot."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 or 404 (no submission for this slot yet)', () =>",
              "  pm.expect(pm.response.code).to.be.oneOf([200,404]));"
            ]
          }
        }
      ]
    },
    {
      "name": "12 — Update a reference (PATCH)",
      "request": {
        "method": "PATCH",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/references/{{referenceId}}",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "references",
            "{{referenceId}}"
          ]
        },
        "description": "This is a full REPLACE, not a partial update — every field you omit is written as null/cleared. `referee_email` is always required. The FIRST call that captures an email for a referee who had none triggers the actual send (unless send:false) — request 08 above never sends by itself.\n\n`template_id` is the ONE exception to the full-replace rule: `null` (shown here) explicitly UNPINS an existing template, a string PINS one (get an id from request 14, List reference templates), and omitting the key entirely leaves an existing pin untouched.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"referee_email\": \"referee@acme.example\",\n  \"referee_name\": \"Jane Referee\",\n  \"referee_organisation\": \"Acme Care Ltd\",\n  \"send\": false,\n  \"template_id\": null\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "console.log('send:', JSON.stringify(pm.response.json().data.send));"
            ]
          }
        }
      ]
    },
    {
      "name": "13 — List candidates",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates?limit=25",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates"
          ],
          "query": [
            {
              "key": "limit",
              "value": "25"
            }
          ]
        },
        "description": "Enumerate this account's candidates, keyset-paginated. Iterate until next_cursor is null, never until candidates is empty — a ?status filter can legitimately return an empty page with more still to come."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "const d = pm.response.json().data;",
              "console.log('candidates:', d.candidates.length, 'next_cursor:', d.next_cursor);"
            ]
          }
        }
      ]
    },
    {
      "name": "14 — List reference templates",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/reference-templates",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "reference-templates"
          ]
        },
        "description": "Lists your own recruiter-authored referee-email templates. Pass an `id` from here as `template_id` on request 08 (Add a reference) or request 12 (Update a reference) to pin it to a specific request. A fresh account with no templates yet gets back an empty `templates` array — that is not an error."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "const templates = pm.response.json().data.templates;",
              "console.log('templates:', JSON.stringify(templates));",
              "const def = templates.find((t) => t.is_default) || templates[0];",
              "if (def) {",
              "  pm.collectionVariables.set('templateId', def.id);",
              "  console.log('templateId =', def.id);",
              "}"
            ]
          }
        }
      ]
    },
    {
      "name": "15 — Retrieve messages",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates/{{candidateId}}/messages",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates",
            "{{candidateId}}",
            "messages"
          ],
          "query": [
            {
              "key": "limit",
              "value": "50",
              "disabled": true
            },
            {
              "key": "cursor",
              "value": "",
              "disabled": true
            }
          ]
        },
        "description": "The candidate's WhatsApp conversation history, newest first, plus a bot_health summary (status, reply_state, transport_ok). Both query params are optional (limit defaults to 50) — enable them in the Params tab to try pagination. Page with `?cursor` using the response's next_cursor until it comes back null — that marks the end, not an empty messages array. A file-type row's body is the candidate's own caption text, never a URL."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "const d = pm.response.json().data;",
              "console.log('messages:', d.messages.length, 'next_cursor:', d.next_cursor);",
              "console.log('bot_health:', JSON.stringify(d.bot_health));"
            ]
          }
        }
      ]
    },
    {
      "name": "16 — Get candidate CV",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates/{{candidateId}}/cv",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates",
            "{{candidateId}}",
            "cv"
          ]
        },
        "description": "Streams the bytes of the candidate's uploaded CV. 404 when this candidate has no CV on file — request 01 above does not attach one (there is no CV field on a JSON create body); attach one via a separate multipart POST /api/v1/candidates with a cv_file field first."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 or 404 (no CV on file)', () =>",
              "  pm.expect(pm.response.code).to.be.oneOf([200,404]));"
            ]
          }
        }
      ]
    },
    {
      "name": "17 — Get assistant name",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/settings/assistant",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "settings",
            "assistant"
          ]
        },
        "description": "Account-level, not candidate-level — no {{candidateId}} in this path. null means no override is set — the account's agency label is used instead."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "console.log('name:', JSON.stringify(pm.response.json().data.name));"
            ]
          }
        }
      ]
    },
    {
      "name": "18 — Patch assistant name (clear, SAFE)",
      "request": {
        "method": "PATCH",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/settings/assistant",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "settings",
            "assistant"
          ]
        },
        "description": "null clears the override and is never gated — always succeeds regardless of plan or WhatsApp-sender configuration, which is why this sample is safe to run as-is. Setting a real (non-null) name is checked against your plan and sender configuration and can 403.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"name\": null\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));"
            ]
          }
        }
      ]
    },
    {
      "name": "19 — Get application-form link (none published yet)",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/application-form/link",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "application-form",
            "link"
          ]
        },
        "description": "The account's one stable public application-form link. `url`/`slug` are both null until request 20 (Ensure application-form link) has run at least once."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "console.log('link before ensure:', JSON.stringify(pm.response.json().data));"
            ]
          }
        }
      ]
    },
    {
      "name": "20 — Ensure application-form link (idempotent mint)",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/application-form/link",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "application-form",
            "link"
          ]
        },
        "description": "Mints the account's one stable link if none exists yet, or returns the SAME existing one — safe to run more than once. There is no rotate verb on this API on purpose: rotating would break every careers page already carrying the old URL."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "const d = pm.response.json().data;",
              "pm.test('url is set', () => pm.expect(d.url).to.be.a('string'));",
              "console.log('application form link:', d.url);"
            ]
          }
        }
      ]
    },
    {
      "name": "21 — Mint a campaign application-form link",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/application-form/links",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "application-form",
            "links"
          ]
        },
        "description": "Mints a fresh campaign link with its own document checklist — a separate, mintable-many-times sibling of the single account link above. Saves the id to {{applicationFormLinkId}} for request 23 (Revoke application-form link).",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"documents\": [\"DBS\", \"Right to Work\"],\n  \"label\": \"Postman test campaign\"\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "const d = pm.response.json().data;",
              "pm.collectionVariables.set('applicationFormLinkId', d.id);",
              "console.log('applicationFormLinkId =', d.id);"
            ]
          }
        }
      ]
    },
    {
      "name": "22 — List application-form links",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/application-form/links",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "application-form",
            "links"
          ]
        },
        "description": "Every campaign link this account has minted, most recent first — INCLUDING revoked ones (each row carries a `revokedAt`, null while still live)."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "console.log('campaign links:', pm.response.json().data.length);"
            ]
          }
        }
      ]
    },
    {
      "name": "23 — Revoke application-form link",
      "request": {
        "method": "DELETE",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/application-form/links",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "application-form",
            "links"
          ]
        },
        "description": "Revokes the campaign link minted by request 21 (Mint a campaign application-form link). Re-revoking an already-revoked link is a harmless no-op.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"linkId\": \"{{applicationFormLinkId}}\"\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "pm.test('revoked', () => pm.expect(pm.response.json().data.revoked).to.eql(true));"
            ]
          }
        }
      ]
    },
    {
      "name": "24 — Send a message",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates/{{candidateId}}/messages",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates",
            "{{candidateId}}",
            "messages"
          ]
        },
        "description": "Sends a free-text WhatsApp message to the candidate, on the recruiter's behalf. Requires consent already granted and a fresh (< 24h) session window — see the docs site's own 409 table for every other refusal. Check delivery later via request 15 (Retrieve messages), matching on the returned message_id.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"body\": \"Thanks for sending your DBS — we just need Proof of Address next.\"\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 or a documented refusal', () =>",
              "  pm.expect(pm.response.code).to.be.oneOf([200,400,404,409,429,500,502,503]));"
            ]
          }
        }
      ]
    },
    {
      "name": "25 — Pause or resume the bot",
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/candidates/{{candidateId}}/bot",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "candidates",
            "{{candidateId}}",
            "bot"
          ]
        },
        "description": "action: \"pause\" takes the conversation over for a human (the API-key door onto the dashboard's \"Take over\"); \"resume\" hands it back to the bot. Requires consent already granted for \"pause\" — see the docs site's own 409 table.",
        "body": {
          "mode": "raw",
          "raw": "{\n  \"action\": \"pause\"\n}"
        }
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200 or a documented refusal', () =>",
              "  pm.expect(pm.response.code).to.be.oneOf([200,400,404,409,429,500,502,503]));"
            ]
          }
        }
      ]
    },
    {
      "name": "26 — List approved templates",
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "X-Api-Key",
            "value": "{{apiKey}}"
          }
        ],
        "url": {
          "raw": "{{baseUrl}}/api/v1/templates",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "api",
            "v1",
            "templates"
          ]
        },
        "description": "Lists the platform's approved WhatsApp templates — what can be sent outside the 24-hour messaging window. Not candidate-scoped; no Content SID is included."
      },
      "response": [],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('200', () => pm.response.to.have.status(200));",
              "const templates = pm.response.json().data.templates;",
              "console.log('approved templates:', templates.length);"
            ]
          }
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://rca-onboardly.com"
    },
    {
      "key": "apiKey",
      "value": "onb_live_REPLACE_ME"
    },
    {
      "key": "candidateId",
      "value": ""
    },
    {
      "key": "idempotencyKey",
      "value": ""
    },
    {
      "key": "lastIdempotencyKey",
      "value": ""
    },
    {
      "key": "referenceId",
      "value": ""
    },
    {
      "key": "documentId",
      "value": ""
    },
    {
      "key": "templateId",
      "value": ""
    },
    {
      "key": "applicationFormLinkId",
      "value": ""
    }
  ]
}
