{
  "info": {
    "name": "Meser 10 JSON API",
    "_postman_id": "8f3b1c22-4d7e-4a19-9f0a-1b5c7e2d9a41",
    "description": "Transactional SMS and email, and contact management, over the Meser 10 JSON gateway.\n\n## Before you run anything\n\n1. Paste your key into the `apiKey` variable, as a collection or an environment variable. It is issued per account in the Meser 10 interface under account settings, advanced settings, API settings.\n2. Set `toPhone` and `toEmail` to your own number and address, and `statusTestEmail` to a throwaway address. **The send requests deliver to real recipients and spend credit.**\n3. Set `senderName` to a sender identity already approved on your account, and `listName` to a list that already exists on it.\n\nStart with **Verify the key**. It creates nothing and sends nothing.\n\n**Run these one at a time. Do not use the Collection Runner.** Top to bottom it would send a real SMS, deliver a real email, and leave an address unsubscribed on your production account.\n\n## Behaviours that commonly cause integration failures\n\n**HTTP 200 is returned for every outcome, including a rejected key.** `ErrorCode` is the only reliable indicator. It comes back as a number on some functions and as a string on others, so normalise before comparing. The tests here do it for you, and they coerce `StatusID` too.\n\n**Never retry an authentication failure.** Repeated failures block the calling IP address for several hours, and the block is on the address, not the key, so reissuing the key and trying again makes it worse. `ErrorCode` 1 is fatal: stop.\n\n**Set a User-Agent.** The hosts sit behind Cloudflare with Browser Integrity Check on, and it reads that header. Default library signatures are blocked: `Java/1.8.0_241` answers 403, `Python-urllib/3.x` answers Cloudflare error 1010. Any custom string passes.\n\n**`Result` is a human-readable message, in Hebrew on most failures.** Branch on `ErrorCode` and supply your own wording. Messages quoted in these request descriptions are English translations, not the strings on the wire, so do not match on them.\n\n## What is not here\n\nCampaigns, mailing lists, groups, reporting and attachments are on the SOAP service at `https://ns.mesereser.com/Services/Services.asmx?wsdl`, which uses the same key.\n\nThere are no webhooks and no per-message delivery status. `SendSingleSmsMessage` returns a `MessageID`, but it comes back as 0 and nothing on this gateway reads a message's delivery state.\n\nFull reference: https://www.meser10.co.il/en/json-api/\nOpenAPI description: https://www.meser10.co.il/api-docs/meser10-json-api.yaml\nSupport: support@meser10.co.il",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    { "key": "endpointUrl", "value": "https://heb.mesereser.com/Services/JsonServices.aspx", "type": "string" },
    { "key": "apiKey", "value": "", "type": "string" },
    { "key": "userAgent", "value": "postman-meser10/1.0", "type": "string" },
    { "key": "toPhone", "value": "0501234567", "type": "string" },
    { "key": "toEmail", "value": "you@example.com", "type": "string" },
    { "key": "statusTestEmail", "value": "throwaway@example.com", "type": "string" },
    { "key": "senderName", "value": "MyShop", "type": "string" },
    { "key": "replyTo", "value": "orders@example.com", "type": "string" },
    { "key": "listName", "value": "Newsletter", "type": "string" },
    { "key": "otp", "value": "", "type": "string" }
  ],
  "auth": {
    "type": "apikey",
    "apikey": [
      { "key": "key", "value": "ApiKey", "type": "string" },
      { "key": "value", "value": "{{apiKey}}", "type": "string" },
      { "key": "in", "value": "header", "type": "string" }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// pm.variables resolves the whole scope chain, so the key may live in an",
          "// environment or a vault rather than in collection scope.",
          "if (!pm.variables.get('apiKey')) {",
          "    throw new Error('Set apiKey as a collection or environment variable before running a request.');",
          "}"
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// HTTP 200 is returned for every outcome, so the status code proves nothing.",
          "pm.test('answered 200', () => pm.response.to.have.status(200));",
          "",
          "let body = null;",
          "try { body = pm.response.json(); } catch (e) { body = null; }",
          "",
          "if (body === null) {",
          "    pm.test('body is JSON', () => {",
          "        throw new Error('The body was not JSON. If it is HTML, Cloudflare refused the request over the User-Agent header.');",
          "    });",
          "    return;",
          "}",
          "",
          "const code = String(body.ErrorCode);",
          "",
          "if (code === '1') {",
          "    pm.test('AUTHENTICATION FAILED - stop here', () => {",
          "        throw new Error('ErrorCode 1. The key was rejected. Do NOT retry: repeated failures block this IP address for several hours, and the block is on the address, not the key.');",
          "    });",
          "    return;",
          "}",
          "",
          "pm.test('ErrorCode is 0 (' + (body.Result || '') + ')', () => pm.expect(code).to.eql('0'));"
        ]
      }
    }
  ],
  "item": [
    {
      "name": "Verify the key",
      "request": {
        "method": "GET",
        "header": [{ "key": "User-Agent", "value": "{{userAgent}}" }],
        "url": {
          "raw": "{{endpointUrl}}?f=GetContactStatus&email=nobody.probe@example.invalid",
          "query": [
            { "key": "f", "value": "GetContactStatus" },
            { "key": "email", "value": "nobody.probe@example.invalid" }
          ]
        },
        "description": "Creates nothing, sends nothing, spends nothing.\n\nA valid key answers `ErrorCode` 0 with `StatusID` 0, meaning the address is not on the account. A rejected key answers `ErrorCode` 1.\n\nRun this first, and run it whenever you suspect a key problem, rather than retrying a real call, which is what creates the IP block."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "let body = null;",
              "try { body = pm.response.json(); } catch (e) { return; }",
              "if (String(body.ErrorCode) === '0') {",
              "    pm.test('key accepted, and StatusID 0 means the probe address is not on the account',",
              "        () => pm.expect(Number(body.StatusID)).to.eql(0));",
              "}"
            ]
          }
        }
      ]
    },
    {
      "name": "Send an SMS one-time password",
      "request": {
        "method": "POST",
        "header": [
          { "key": "Content-Type", "value": "application/json; charset=utf-8" },
          { "key": "User-Agent", "value": "{{userAgent}}" }
        ],
        "url": {
          "raw": "{{endpointUrl}}?f=SendSingleSmsMessage",
          "query": [{ "key": "f", "value": "SendSingleSmsMessage" }]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"ToPhone\": \"{{toPhone}}\",\n  \"MessageBody\": \"Your code is {{otp}}. It expires in 5 minutes.\",\n  \"FromName\": \"{{senderName}}\"\n}",
          "options": { "raw": { "language": "json" } }
        },
        "description": "One recipient, sent immediately. **This delivers to a real phone and spends credit.**\n\nThe pre-request script generates a six-digit code into the `otp` variable, so the request runs as it stands.\n\n`FromName` has to be a sender identity already approved on the account. Two forms are accepted: an alphanumeric sender name of up to **11 characters**, Latin letters, digits and spaces only, containing at least one letter; or a number, in local or E.164 form. Hebrew text and digits-only names are rejected. The 11-character limit is a GSM constraint on alphanumeric sender IDs, not a Meser 10 limit, and support varies by destination country: alphanumeric sender IDs are not available in the United States or Canada, where a number is used instead.\n\nUntil the identity is approved you get `ErrorCode` 4 with the Hebrew equivalent of \"the SMS sender identity is not verified\".\n\nHebrew text is sent as Unicode, which shortens a single-part message from 160 characters to 70.\n\nNote what you do **not** get back: a delivery state. `MessageID` comes back as 0."
      },
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.collectionVariables.set('otp', String(Math.floor(100000 + Math.random() * 900000)));"
            ]
          }
        }
      ]
    },
    {
      "name": "Send a transactional email",
      "request": {
        "method": "POST",
        "header": [
          { "key": "Content-Type", "value": "application/json; charset=utf-8" },
          { "key": "User-Agent", "value": "{{userAgent}}" }
        ],
        "url": {
          "raw": "{{endpointUrl}}?f=SendSingleEMailMessage",
          "query": [{ "key": "f", "value": "SendSingleEMailMessage" }]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"ToEMail\": \"{{toEmail}}\",\n  \"Subject\": \"Your receipt\",\n  \"Body\": \"<p>Thank you for your order.</p>\",\n  \"FromName\": \"{{senderName}}\",\n  \"ReplyToEMail\": \"{{replyTo}}\"\n}",
          "options": { "raw": { "language": "json" } }
        },
        "description": "One recipient, sent immediately. **This delivers to a real mailbox.**\n\n`ReplyToEMail` is **required**, even though older documentation shows it as an empty string. Omit it and the call answers a null reference error with `ErrorCode` 3.\n\nWhat this function does not accept: a From address, only a display name; no CC or BCC; no attachments; and no more than one recipient. Check for all four before calling and fall back to your own transport when you see one.\n\nFor Hebrew content, set `dir=\"rtl\"` inside your own HTML. Nothing does it for you.\n\n`LinkClickCountType` is optional. Omit it, or send an empty string, unless support has told you otherwise."
      }
    },
    {
      "name": "Create or update a contact",
      "request": {
        "method": "POST",
        "header": [
          { "key": "Content-Type", "value": "application/json; charset=utf-8" },
          { "key": "User-Agent", "value": "{{userAgent}}" }
        ],
        "url": {
          "raw": "{{endpointUrl}}?f=CreateContact",
          "query": [{ "key": "f", "value": "CreateContact" }]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"ContactListName\": \"{{listName}}\",\n  \"EMail\": \"{{statusTestEmail}}\",\n  \"PhoneNo\": \"{{toPhone}}\",\n  \"FirstName\": \"Dana\",\n  \"LastName\": \"Levi\"\n}",
          "options": { "raw": { "language": "json" } }
        },
        "description": "Subscribes a contact to a named list, or updates them if they already exist. This writes to your production account.\n\nThirteen fields are accepted: `ContactListName`, `EMail`, `PhoneNo`, `FirstName`, `LastName`, `Address`, `City`, `Zipcode`, and `CustomField1` to `CustomField5`. Spelled exactly like that, and note the capitalisation of `EMail` and `PhoneNo`. Omit fields you are not sending rather than sending empty strings.\n\n`ContactListName` is required, plus at least one of `EMail` or `PhoneNo`.\n\n**The list is not created for you.** A list that does not exist on the same account as the key answers `ErrorCode` 4 with the Hebrew equivalent of \"contact list does not exist: <name>\". Show that as \"that list was not found on your account\", not as a generic failure."
      }
    },
    {
      "name": "Change a contact's status",
      "request": {
        "method": "POST",
        "header": [
          { "key": "Content-Type", "value": "application/json; charset=utf-8" },
          { "key": "User-Agent", "value": "{{userAgent}}" }
        ],
        "url": {
          "raw": "{{endpointUrl}}?f=ChangeContactStatus",
          "query": [{ "key": "f", "value": "ChangeContactStatus" }]
        },
        "body": {
          "mode": "raw",
          "raw": "{\n  \"EMail\": \"{{statusTestEmail}}\",\n  \"Status\": \"Unsubscribed\"\n}",
          "options": { "raw": { "language": "json" } }
        },
        "description": "**This unsubscribes a real address on your production account.** It points at `statusTestEmail` rather than `toEmail` for that reason. An unsubscribed address is suppressed for later sends, so do not point it at an address you test delivery with.\n\nIdentify the contact by either `EMail` or `PhoneNo`. `Status` is one of `Active`, `Unsubscribed` or `Bounced`; anything else answers `ErrorCode` 4 with the Hebrew equivalent of \"unknown value of status\".\n\nOne behaviour to plan for: an address that is not on the account **also** answers `ErrorCode` 0. A successful call is not proof that the contact existed, so do not use this as an existence check. Use **Read a contact's status** for that."
      }
    },
    {
      "name": "Read a contact's status",
      "request": {
        "method": "GET",
        "header": [{ "key": "User-Agent", "value": "{{userAgent}}" }],
        "url": {
          "raw": "{{endpointUrl}}?f=GetContactStatus&email={{statusTestEmail}}",
          "query": [
            { "key": "f", "value": "GetContactStatus" },
            { "key": "email", "value": "{{statusTestEmail}}" }
          ]
        },
        "description": "The only read on the gateway, and the one function called differently: the address goes on the **query string**, not in a JSON body. Every JSON body spelling answers the Hebrew equivalent of \"email parameter is empty\".\n\nOnly `email` is supported. A `phone` parameter is not read and returns the same error.\n\nBranch on `StatusID`, never on `Status`, which is display text and may be absent:\n\n| Id | Meaning |\n|---|---|\n| 0 | Not on this account |\n| 10 | Active |\n| 30 | Active, after being reactivated |\n| 40 | Bounced |\n| 50 | Unsubscribed |\n\n**10 and 30 both mean mailable.** A contact created through the API returns 10; one moved back to Active after a bounce or an unsubscribe returns 30. Treat both as mailable or you will silently drop reactivated contacts."
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "let body = null;",
              "try { body = pm.response.json(); } catch (e) { return; }",
              "if (String(body.ErrorCode) === '0') {",
              "    const id = Number(body.StatusID);",
              "    const mailable = [10, 30].includes(id);",
              "    pm.test('StatusID ' + id + ' -> ' + (mailable ? 'mailable' : 'not mailable'),",
              "        () => pm.expect(body).to.have.property('StatusID'));",
              "}"
            ]
          }
        }
      ]
    }
  ]
}