NAV
shell

Introduction

Welcome to the documentation for the MoyaApp Business Messaging API. If you run into any issues, or have questions or feedback, please let us know by sending a message to api-support@moya.app.

You can find extensive working sample code in Python in our GitHub repository. You can access many features of this API on our front end site.

Bot service integration

We provide a separate service to integrate directly with several common messaging platforms. If you wish to use this, please contact us to discuss options rather than trying to write your own integration.

The platforms we currently support are:

Authentication

To access the Messaging API, you will need an access token. In order to obtain one, please send a message to api-support@moya.app specifying the mobile number that you would like to use.

The MoyaApp Messaging API uses Bearer tokens for authentication (please see RFC-6750 standard for more information). You can pass the token in one of two ways:

Sending Messages

To send a message, simply send a POST request to /v1/message as shown in the example.

This endpoint will send to any number that you specify, even if it is not on our system, or not active. For this reason we strongly recommend that you first filter for active Moya numbers using the API calls available.

Example Message:

Permission Required :SEND

HTTP Request

POST /v1/message

Example Request

curl --location --request POST 'https://api.moya.app/v1/message' \
  --header 'Authorization: Bearer YourSecretToken' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "to":"27123456789",
    "recipient_type":"individual",
    "type":"text",
    "text": {
      "body":"Hello World"
    }
  }'

Example Response

{
  "id":"1ec019dc-c65f-6614-9d00-7f01b918938a"
}

JSON Parameters

Name Type Required Description
to String Yes This field is for the phone number of the user that you wish to message.
recipient_type String Yes Must be set to "individual" or "broadcast". See Broadcast Messages for more detail
priority String No See the Priority section below. Must be either "low", "medium", "high". Defaults to "medium"
deduplication_id String No See the Deduplication section below. Must be a valid UUID when set
type String Yes "text" or "image". See the below sections for the additional parameters required depending on the message type

Text message parameters

Name Type Required Description
text JSON Object Yes Contains the body field below.
text.body String Yes The message body goes here. No more than 2048 characters.
text.responses JSON Object No Suggested quick responses. See Quick Responses section
text.allow_merge boolean No Default true. By default MoyaApp collapses messages that were sent in relative quick succession into one message bubble. Setting this flag to false disables this behaviour.

Image message parameters

Name Type Required Description
image JSON Object Yes Contains the url field below.
image.url String Yes The URL previously retrieved through the upload_image endpoint

Query Parameters

Name Type Required Description
job_id String No See the Jobs section below. Must be a valid UUID when set

Result

The MoyaApp Messaging API will signal success with an HTTP Status code 200 and a JSON object with a parameter id containing the id of the queued message.

Error Result

An error will be signaled with HTTP Status codes 4xx and 5xx and a JSON payload with more details. The JSON paloay is a complex object with the following parameters.

Example Error Response

{
  "error": "MISSING_ARGUMENT",
  "message": "Missing 'to' parameter"
}
Name Type Required Description
error Strinng Yes One of the following: MISSING_TOKEN, INVALID_TOKEN, INVALID_JSON, MISSING_ARGUMENT, INTERNAL_SERVER_ERROR, TOO_MANY_REQUESTS, SERVICE_UNAVAILABLE
message String No A detailed, human readable description of the error

Formatting

You may want to add some formatting to your messages, for example bold or italic. The MoyaApp Android client fully implements the XEP-0393 spec to allow this. The MoyaApp iOS client implements some of the more commonly used features of this.

Formatting Android iOS Example content Example render
Bold Yes Yes *text* text
Italic Yes Yes _text_ text
Quoting Yes No > text | text
> > text | | text
Strikethrough Yes No ~text~ text
Monospace Yes No `text` text
Split message
(into multiple message bubbles)
Yes No Hi___---___How are you? Hi

How are you?

Note that the formatting characters stay visible to the user, until MoyaApp app version 4.5. If you want to send messages without visible formatting marks in older app versions, you can try using Unicode formatting, however only a subset of these codes will render correctly and display may be different between different OS and app versions.

A newline in the content creates a newline in the message. Use two newlines to create a new paragraph.

Emojis are fully supported and standard across all client versions on a given platform.

We advise that before sending out significant volumes of messages containing formatting you should test on multiple devices to ensure they are rendering as you wish, especially if you are intending to use Unicode formatting.

All formatting characters are included in the maximum size of a message (2048 bytes).

Web links ie http:// and https:// reproduced verbatium, and are colored blue and clickable. They will open according to the standard rules of the host operating system (ie usually in a browser, but some can be captured by apps and redirected to them directly).

We support various MoyaApp in-app links when a link begins with http://moya.me/ or moya://.

Phone numbers

If the content is a phone number of the format +12345 then it will be rendered just as the phone number and upon click will open up a chat session within MoyaApp going to that number. For example https://moya.me/+12345 would render as +12345. Passing a text query paramater allows you to pre-populate the chat message with that content. For example moya://+12345?text=A%20sample%20message would render as +12345 and upon clicking a chat will be started with that number and the text box is prepopulated with "A sample message".

Otherwise, if the content of the link is a recognised in-app MoyaApp link, for example to a Discover app or some internal part of MoyaApp, it will be displayed as the text of that link. For example moya://moya.payd would render as MoyaPay where the link would open the MoyaPay section of the app.

If you have an app in Discover, our support team can advise you of the moyaKey which it has been assigned which can be used to link directly to it from a chat message.

Quick Responses

You can add quick responses to text messages. Those appear as buttons to the user. To accomplish that simply add a responses array inside the text element. The responses array contains an arbitrary number of response objects each consisting of the parameters response, color, background_color. The color paramaters are optional.

curl --location --request POST 'https://api.moya.app/v1/message?job_id=59102c4f-084d-4566-b0ae-4fed6621cf14' \
  --header 'Authorization: Bearer YourSecretToken' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "to":"27123456789",
    "recipient_type":"individual",
    "type":"text",
    "text": {
      "body":"Are you interested in earning extra money with MoyaApp?đź’°",
      "responses": [
        {
          "response": "Yebo",
          "color": "#00ff00"
        },
        {
          "response": "Not Really",
          "background_color": "#ff0000",
          "color": "#ffffff"
        }
      ]
    }
  }'

Example Quick Response:

Response parameter

Name Type Required Description
response String Yes The suggested response
color String (hex color value) No The foreground (text) color of the button
background_color String (hex color value) No The background color of the button

Request Location

Requesting a user's location can be done by including one of two uris in message body sent to the user. What the user sees will depend on the uri and also the MoyaApp version installed. An older version will display just the text on the message body and not the auto trigger button, so it's advisable to be descriptive and clear on what you want the user to do (as well as including any other information) in the message body before appending the uri, this will allow the user to get more information (probably why you are asking for their location) as well as display the location share button if able.

Both responses will be received in the form of a geo URI. as described here

HTTP Request

POST /v1/message

Example Request

curl --location --request POST 'https://api.moya.app/v1/message' \
  --header 'Authorization: Bearer YourSecretToken' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "to":"27123456789",
    "recipient_type":"individual",
    "type":"text",
    "text": {
      "body":"Please share your location moya.location.request.gps"
    }
  }'

Request Date and Time

Supported from MoyaApp Android version 6.3.0

One can also request a user provides their date, time or datetime information by including one of the below uris in the message body, and just like requesting location, the user sees a button (depending on their MoyaApp version) or the uri itself together with any other information supplied. On click of the button, a calendar, time or both prompts are displayed to allow the user send back the required information in the standard format specified below.

Name URI Response Format
Date And Time moya.calendar.request yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
Date moya.calendar.request.date yyyy-MM-dd
Time moya.calendar.request.time HH:mm

Example Request

curl --location --request POST 'https://api.moya.app/v1/message' \
  --header 'Authorization: Bearer YourSecretToken' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "to":"27123456789",
    "recipient_type":"individual",
    "type":"text",
    "text": {
      "body":"Please share your current date and time moya.calendar.request"
    }
  }'

Deduplication

To handle timeouts on the HTTP request where the sender doesn’t know if the message was accepted or not the sender can optionally include a deduplication_id in the request. When included the deduplication_id must be a valid UUID otherwise the request will be rejected.

If the MoyaApp Messaging API notices a previously used tuple of recipient (to) and deduplication_id it will not send the message again but instead return the ID of the previously sent message.

Priority

Each sending account has an outgoing message queue. The message queue drains at a fixed rate. Messages in the queue are ordered by their priority and their time of submission. The default priority is medium. If you always use the default priority you can put messages to the front of the queue by giving them a priority of high.

If, for example, you are sending both conversational messages and mass messages at the same time and you want to ensure that conversational messages arrive as soon as possible you might want to tag your conversational messages as high and all mass/broadcast messages with a priority of low.

Jobs

You can batch groups of related messages by specifying a job_id UUID query parameter (not a JSON parameter). For example if sending a mailshot to 1000 customers you could use the same job_id. This can be specified for both message and messages endpoints.

The advantages of using a job_id are: - The ability to cancel jobs - Reporting will give you details of per-batch

Permission Required :SEND

Example Request

curl --location --request POST 'https://api.moya.app/v1/message?job_id=59102c4f-084d-4566-b0ae-4fed6621cf14' \
  --header 'Authorization: Bearer YourSecretToken' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "to":"27123456789",
    "recipient_type":"individual",
    "type":"text",
    "text": {
      "body":"Hello World"
    }
  }'

Broadcast messages

The same message can be send to up to 10,000 recipients with one request. To send a message to more than one recipient set the recipient_type attribute to broadcast and use an array of multiple recipients as the to attribute. This works for both text and image messages. If you want to send different messages to multiple recipients, please see the section below on Bulk Sending Messages.

Permission Required :SEND

Example Broadcast request

curl --location --request POST 'https://api.moya.app/v1/message' \
  --header 'Authorization: Bearer YourSecretToken' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "to":["27123456789","27222222111"],
    "recipient_type":"broadcast",
    "priority": "low",
    "type":"text",
    "text": {
      "body":"Hello World"
    }
  }'

Example Broadcast response

{
  "id": [
    "1ec79d43-8ec0-6ac8-a30b-dbcc9e031abd",
    "1ec79d43-8ec0-6ac9-a30b-dbcc9e031abd"
  ]
}

Sending Images

Sending images is a two-step process:

  1. The image is uploaded with the upload_image endpoint documented below. A #datafree optimized webp image URL is returned.
  2. The image is sent with the message endpoint as documented. The #datafree optimized image URL from step (1) needs to be included in a message. The same uploaded image / URL can be used and send with multiple messages (See the next section).

Uploading an image

The first endpoint contains a file field to upload an image with a file extension of jpg, png, webp, gif, svg or bmp directly.

The endpoint then converts the image to an optimized image and returns a link to it. Please note that this link is only valid for 30 days.

Initially this link should be checked manually once to ensure that the optimized image displays correctly and all text, etc... is sufficiently legible before sending it out.

Example Image Send:

Permission Required :IMAGE_UPLOAD

HTTP Request

POST /v1/upload_image

Example 1: Uploads an image:

curl --location --request POST 'https://api.moya.app/v1/upload_image' \
--header 'Authorization: Bearer <YourSecretToken>' \
--form 'file=@"<ImageLocation>"' \

Example 1: Successful Response:

{
    "url": "<DataFreeURL>/<YourBotNumber>/f9919ca9-5d57-49ae-ad91-329ffb23bdc9.webp"
}

Example 2: Uploads an image with public url and extension (either jpg, png, webp, gif, svg or bmp) :

curl --location --request POST 'https://api.moya.app/v1/upload_image' \
--header 'Authorization: Bearer <YourSecretToken>' \
--form 'url="<PublicURL>"' \

Example 2: Successful Response:

{
    "url": "<DataFreeURL>/<YourBotNumber>/f9919ca9-5d57-49ae-ad91-329ffb23bdc9.webp"
}

Example 3: Error message, if file size is too big:

curl --location --request POST 'https://api.moya.app/v1/upload_image' \
--header 'Authorization: Bearer <YourSecretToken>' \
--form 'file=@"<ImageLocation>"' \

Example 3: Error response:

{
    "errors": [
        {
            "status": 413,
            "detail": "Maximum size is 1048576 bytes."
        }
    ]
}

Form Parameters

Name Type Required Description
file File file or url Location to the file with a file extension of either jpg, png, webp, gif, svg or bmp.
url Text file or url Public file to url with file extension (currently only jpg, png, webp, gif, svg or bmp is allowed).

Sending Files

Sending files #datafree is a two-step process:

  1. The file needs to be uploaded with the upload_file endpoint documented below. A #datafree URL will be returned.
  2. The file is sent with the message endpoint as documented. The #datafree file URL from step (1) needs to be included in a message. The file URL can be used and send with multiple messages (See the next section).

Uploading a file

The first endpoint contains a file field to upload a file directly (preferred method) or using a url (requires an extension (Only .pdf is currently supported)).

This endpoint returns a link to the #datafree file. Please note that this link is only valid for 30 days.

Example File Message:

Permission Required :FILE_UPLOAD

HTTP Request

POST /v1/upload_file

Example 1: Uploading a file (preferred method):

curl --location --request POST 'https://api.moya.app/v1/upload_file' \
--header 'Authorization: Bearer <YourSecretToken>' \
--form 'file=@"<FileLocation>"' \

Example 1: Successful Response:

{
    "url": "<DataFreeURL>/<YourBotNumber>/f9919ca9-5d57-49ae-ad91-329ffb23bdc9.pdf"
}

Example 2: Uploading a file with an url:

curl --location --request POST 'https://api.moya.app/v1/upload_file' \
--header 'Authorization: Bearer <YourSecretToken>' \
--form 'url="<PublicURL>"' \

Example 2: Successful Response:

{
    "url": "<DataFreeURL>/<YourBotNumber>/f9919ca9-5d57-49ae-ad91-329ffb23bdc9.pdf"
}

Example 3: Error message, if file size is too big:

curl --location --request POST 'https://api.moya.app/v1/upload_file' \
--header 'Authorization: Bearer <YourSecretToken>' \
--form 'file=@"<ImageLocation>"' \

Example 3: Error response:

{
    "errors": [
        {
            "status": 413,
            "detail": "Maximum size is 1048576 bytes."
        }
    ]
}

Form Parameters

File Parameters

Name Type Required Description
file File file or url Location to the file.
url Text file or url Public file to url with file extension (currently only .pdf is allowed).

Sending an image or file to a user

To send an image/file message, simply send a POST request to /v1/message as shown in the example. Full details of this endpoint can be found in its documentation above. All parameters for text messaging are valid with image/file #datafree attachment.

A type parameter is required: type:image/file specifying whether it's an image or a file.

Next an image/file parameter should be added with a dictionary and an url parameter with the link to the image/file (supply the #datafree url generated from the image_upload/file_upload endpoint). See the example for more information.

Permission Required :SEND

HTTP Request

POST /v1/message

Example 1 Request

curl --location --request POST 'https://api.moya.app/v1/message' \
  --header 'Authorization: Bearer YourSecretToken' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "to":"27123456789",
    "recipient_type":"individual",
    "type":"image",
    "image": {
      "url":"<DataFreeURL>/<YourBotNumber>/f9919ca9-5d57-49ae-ad91-329ffb23bdc9.webp"
    }
  }'

Example 1 Response

{
  "id":"1ec019dc-c65f-6714-9d00-7f01b918938a"
}

Example 2 Request

curl --location --request POST 'https://api.moya.app/v1/message' \
  --header 'Authorization: Bearer YourSecretToken' \
  --header 'Content-Type: application/json' \
  --data-raw '{
    "to":"27123456789",
    "recipient_type":"individual",
    "type":"file",
    "file": {
      "url":"<DataFreeURL>/<YourBotNumber>/f9919ca9-5d57-49ae-ad91-329ffb23bdc9.pdf"
    }
  }'

Example 2 Response

{
  "id":"1ec019dc-c65f-6614-9d00-7f01b918938a"
}

Cancelling In-progress Sending

If you specified a job_id query parameter for your send process, you are able to cancel any which have been queued but not yet sent.

HTTP Request

DELETE /v1/jobs/<job_id>

Example Request

curl --request DELETE --location 'https://api.moya.app/v1/jobs/c1592dbe-ead6-4af3-8f01-0154480379ac' \
  --header 'Authorization: Bearer YourSecretToken' \

Result

The MoyaApp Messaging API will signal success with an HTTP Status code 200 and an empty JSON object ({}).

Bulk Sending Messages

You can send multiple messages (text or image) in a single HTTP request using the messages endpoint

Permission Required :SEND

HTTP Request

POST /v1/messages

Example Request

curl --location --request POST 'https://api.moya.app/v1/messages' \
  --header 'Authorization: Bearer YourSecretToken' \
  --header 'Content-Type: application/json' \
  --data-raw '[
      {
        "to":"27123456789",
        "recipient_type":"individual",
        "type":"text",
        "text": {
          "body":"Hello World"
        }
      },
      {
        "to":"27123456790",
        "recipient_type":"individual",
        "type":"text",
        "text": {
          "body":"Other message"
        }
      }
  ]'

Example Response

[
    {
      "id":"1ec019dc-c65f-6614-9d00-7f01b918938a"
    },
    {
      "id":"1ec019dc-c65f-6614-9d00-7f01b918939c"
    }
]

Parameters

Simply pass an array of objects one per message as specified in the Sending Messages section above. The response is a list of responses in the same order as the messages that were passed.

Additionally you can also pass a query parameter of job_id which is applied to all messages sent by the request.

As with broadcast messages, there is a hard limit of sending to 10,000 recipients per HTTP request. Additionally the total size of the JSON body cannot exceed 10Mb.

Account details

Configuration

The current configuration of an account can be checked via the /v1/configuration endpoint. Simply send an authenticated GET request.

curl 'https://api.moya.app/v1/configuration' \
  --header 'Authorization: Bearer YourSecretToken'

Example Account Details:

Example Response

{
  "configuration": {
    "phone_number": "2700008008",
    "name": "Lesedi from MoyaApp",
    "rate": 2,
    "event_url": "http://localhost:1234/",
    "events": [
      "MESSAGE",
      "STATUS"
    ]
  }
}

Modification

Specific account information can be modified at any time using /v1/update endpoint. Fields allowed to be modified include name, allowed_events, event_url and status_url.

All data fields are optional.

Request json data should contain the required value against the field to be modified, any omitted or null value field will be left unmodified.

Values entered for event_url and status_url should adhere to RFC 6570 URI Template Requirements. Variables may include

If an account's phone number is 27123456789, then http://example.com/v1/webhook/{to} will translate into http://example.com/v1/webhook/27123456789

Permission Required :MODIFY

curl --location --request PUT 'https://api.moya.app/v1/update' \
      --header 'Authorization: Bearer YourSecretToken' \
      --header 'Content-Type: application/json' \
      --data-raw '{
        "name":"newBotName",
        "event_url": "http://example.com/v1/webhook/{to}",
        "allowed_events": [
          "MESSAGE","STATUS"
          ]
      }'

Avatar image

The account's currently configured avatar can be requested via the /v1/avatar endpoint.

If you do not currently have an avatar set, a HTTP 404 error will be returned. If you do have an avatar, the response will be an image (currently png format).

Creating/Modifying avatar image

An avatar can also be created or replaced by sending a PUT request through the /v1/avatar endpoint with the relevant parameters. If you want to manually change your profile image you can do this via the manage tokens page in the business messaging API frontend

Permission Required :MODIFY

curl --location --request PUT 'https://api.moya.app/v1/avatar' --header 'Content-Type: multipart/form-data' 
--header 'Authorization: Bearer YourSecretToken' --form 'file=@/path/to/image/file'

Health

Use the health node to check the status of your MoyaApp Messaging API account.

Permission Required :HEALTH

HTTP Request

GET /v1/health

Example Request

curl --location 'https://api.moya.app/v1/health' \
  --header 'Authorization: Bearer YourSecretToken' \

Example Response

{
  "health": {
    "gateway_status": "connected",
    "ping": {
        "success": true,
        "time": 23
    },
    "delay": {
        "delay": 0,
        "priority": "low"
    },
    "pending_events": 0
  }
}

Result Parameter

Name Type Description
health JSON Object A complex object containing various health parameter
health.gateway_status String 'disconnected', 'connecting', 'connected' indicates if the account is connected to the network
health.ping.success Boolean Is MoyaApp network reachable
health.ping.time Integer Round trip time in miliseconds to MoyaApp server
health.delay.delay Double Time the most recently sent message spent in the queue in seconds
health.delay.priority String Priority of the most recently sent message
health.pending_events Integer Number of events currently queued for delivery to the webhook

Token

Token information can also be fetched. This will give a response containing the jid and activities linked with this token. Activities listed below, represent the available operations allowed for this token

HTTP Request

GET /v1/token/info

Example Request

curl --location 'https://api.moya.app/v1/token/info' \
  --header 'Authorization: Bearer YourSecretToken' \

Example Response

{
  "jid": "27123456789@binu.m.in-app.io",
  "expiry": "2022-09-28T09:17:44",
  "activities": [
    "USER_LOOKUP",
    "SEND",
    "MODIFY"
  ],
  "disabled": false,
}

Result Parameter

Name Type Description
jid String Bot's jid
expiry DateTime Token expiry date. A null/unspecified value means token will never expire
activities Array List of allowed endpoint operations for this token
disabled Boolean Token enabled flag

User lookups

Bulk contact lookups

To check if a list of user phone numbers from your database correspond to valid MoyaApp accounts, please submit a POST request as shown in the examples below. An account is considered valid if the user has been active in the past 90 days.

The /v1/contacts endpoint supports 2 formats in which a list of numbers can be submitted, plaintext and hashed (for added security in transmitting a list of your contacts over the Internet). Please use the hashed parameter (bool), to indicate whether you are submitting a list of hashed or plain numbers.

Example Request 1 with plaintext numbers

curl --location --request POST 'https://api.moya.app/v1/contacts' \
--header 'Authorization: Bearer YourSecretToken' \
--header 'Content-Type: application/json' \
--data-raw '{
  "contacts": [
    "27123456789",
    "27000000000",
    "27987654321"
  ]
}'

Example Response (1 of the submitted numbers identified as a MoyaApp user and returned)

{
    "error": false,
    "match_count": 1,
    "match_list": ["27123456789"]
}

Example Request with hashed numbers

curl --location --request POST 'https://api.moya.app/v1/contacts?last_seen_days_ago=50' \
--header 'Authorization: Bearer YourSecretToken' \
--header 'Content-Type: application/json' \
--data-raw '{
        "contacts": [
        "FD9F304A987B7354A6AF8BF25ECD63DD32F8D7349F6473B890F0320613D04470",
        "555CDE815EF46CC7F2C1B0EAFD78F04E1293061FCC176E6A8202AE1C3A47080F",
        ],
        "hashed":true
}'

Example Response (1 of the submitted numbers identified as a MoyaApp user and returned)

{
    "error": false,
    "match_count": 1,
    "match_list": ["FD9F304A987B7354A6AF8BF25ECD63DD32F8D7349F6473B890F0320613D04470"]
}

Permission Required :USER_LOOKUP

HTTP Request

POST https://api.moya.app/v1/contacts

Parameters

Name Type Required Description
contacts Array Yes Array of phone numbers (up to 1000 at a time) that you are validating. The numbers must be in international format, excluding trailing zeros and plus sign (e.g.: 27123456789). When using hashed parameter, the numbers should be hashed first. If you provide plaintext numbers with this parameter on, lookup will fail.
hashed Boolean Yes Set this to true if the numbers are being transmitted in hashed format. Defaults to false.

You can additionally specify the following query parameters:

Name Type Default Description
last_seen_days_ago integer 90 Ensure user was active within this time period. The minimum value is 7 days.

Here is a diagram representing an example process flow to check for user base overlap, in more detail.

Individual user lookups

You can call this endpoint to find specific details about a user who was active within the last 90 days. You must pass either a phone number or a "MoyaApp Device ID" (also known as a DID) in order to look up this information.

A MoyaApp DID is a UUID which uniquely identifies a user's device install and is a way of securely passing a user between systems without explicitly sending their phone number. You can use this call to take a MoyaApp DID and map it to the users phone number. If the user changes devices or for many other reasons, a DID will reset so these are not intended to be stored for long periods of time but rather used immeditately to look up the user's phone number. For apps in MoyaApp Discover, a DID is automatically added to the query parameters of the url (if the Discover item has been configured with "useDid": true). This is passed as the x-binu-did query parameter.

You can additionally specify the following query parameters:

Name Type Default Description
last_seen_days_ago integer 90 Ensure user was active within this time period. The minimum value is 7 days.

Example 1: Request with a number:

curl -s --header "Authorization: Bearer YourSecretToken" https://api.moya.app/v1/users/27123456789?last_seen_days_ago=50

Example 1: Successful Response:

{
  "user_profile": {
    "number": "27123456789",
    "jid": "27123456789@binu.m.in-app.io",
    "aid": "64afb426-6062-486a-ab89-62e55a780a4e",
    "did": "84eaad4b-d2a4-44f0-8de1-49b11ec6c084",
    "last_seen": "2021-11-15 13:46:31.439036",
    "registration_date": "2020-06-04 19:22:19.141956"
  }, 
  "error": false
}

Example 2: Request with a did (device identifier):

curl -s --header "Authorization: Bearer YourSecretToken" https://api.moya.app/v1/users/84eaad4b-d2a4-44f0-8de1-49b11ec6c084

Example 2: Successful Response:

{
  "user_profile": {
    "number": "27123456789",
    "jid": "27123456789@binu.m.in-app.io",
    "aid": "64afb426-6062-486a-ab89-62e55a780a4e",
    "did": "84eaad4b-d2a4-44f0-8de1-49b11ec6c084",
    "last_seen": "2021-11-15 13:46:31.439036",
    "registration_date": "2020-06-04 19:22:19.141956"
  }, 
  "error": false
}

Example 3: Error message, not found/last_seen more than 90 days:

curl -s --header "Authorization: Bearer YourSecretToken" https://api.moya.app/v1/users/2700000000

Example 3: Error response:

{
  "errors": [
    {"status": 404, "detail": "No such user 27000000000."}
  ]
}

Permission Required :USER_LOOKUP

HTTP Request

GET /v1/users/<reference>

The reference parameter could be either a number or a MoyaApp DID

Parameters

Name Type Required Field Description
reference String Yes (either number or DID) number This field is for the phone number of the requested user.
- - - did This field is for the DID (MoyaApp Device Identifier) of the requested user.

Webhooks

Webhooks are are HTTP callbacks that trigger on specific events. To receive events via your MoyaApp API integration, you need to have configured a URL "inbox" where the events can be sent to (e.g. https://your.webservice.com/secret/moya-event-endpoint). You can configure this URL using the messaging API frontend manage tokens page and clicking the "edit configuration settings" option for the bot.

Incoming events are JSON payloads that will be sent via HTTP POST. Events have a top level field indicating the type of event. Available fields are:

Name Description
message Used to notify about a new message and the content of that message
status Used to notify about status changes in a message you sent

To acknowledge the receipt of an event, please ensure your service responds with HTTP code 200 (OK) within 30 seconds. An error or timeout will show a delivery failure message to the user.

To reduce load on your webhook you don’t need to subscribe to all events. For example if you are not going to process status events you can subscribe to message events only. This is configured via the messaging API frontend manage tokens page.

Message

The message field contains an object with the following attributes:

Attributes

Name Type Description
type String The type of the message. One of: text
from String The phone number of the user that sent the message
text JSON Object Only set when message is of type text.
text.body String The message body of a text message

Incoming messages will be sent via HTTP POST in the format below:

{
  "message":
    {
      "type":"text",
      "from":"27123456789",
      "text":
        {
          "body":"Hello"
        }
     }
}

Receiving location

When a user sends their location, using the 'Attachments / Send location' button in the app, it will be sent as a message event of type text with the body being set to a geo URI.

{
  "message":
    {
      "type":"text",
      "from":"27123456789",
      "text":
        {
          "body":"geo:48.7757715,6.07773784;u=12"
        }
     }
}

If the user hand picked their location, or if a default location has been used due to lack of Location permission, the uncertainty (u) parameter will be omitted from the URI. If the location came directly from GPS the uncertainty parameter will contain the precision in meters. (Lower values are better.). For example geo:48.7757715,6.07773784;u=12

geo:-30.559482,22.937506 is the default location for users in South Africa who didn’t provide location permission and didn’t manually move the position around.

Receive image

When a user sends an image, the webhook body will contain a link to an encrypted version of the image. The image can be decrypted using only the url using the following script: Image Decryptor

Script usage example:

python decrypt-picture.py "aesgcm://..." --output="test.webp"

The script requires the following dependencies:

cryptography==43.0.1
requests==2.32.3

This script can also be modified and used in backend systems. It uses standard encryption algorithms, and can be translated to most languages and frameworks.

Receiving sensitive messages

Sometimes users might send sensitive messages, for example after being prompted for a password, that should not remain part of the on-device chat history. In that case the recipient has the abililty to delete that message from the user’s device. Determination of what constitutes a sensitive messages falls to the recipient.

Response code

The easiest method to achieve this is to respond with HTTP code 205 (Reset Content) on the message web hook.

URL HTTP Request

POST /v1/received-sensitive

JSON Parameters

Name Type Required Description
from String Yes The phone number of the message the sensitive content was received from. Copied from the from attribute of the message event.
id String Yes The id of the sensitive message. Copied from the id attribute of the message event.

Status

The status field contains an object with the following parameters:

Incoming message status update will be sent via HTTP POST in the format below:

{
  "status": {
    "id": "1ec019dc-c65f-6614-9d00-7f01b918938a",
    "recipient_id": "27123456789",
    "status": "sent"
  }
}

Parameters

Name Type Description
id String A UUIDv6. Matches the id returned upon sending a message
recipient_id String The phone number the message was sent to
status String One of the following: sent, delivered_to_user, read, delivery_failed

Status field meanings

Name Meaning
sent Message has been sent from our internal queue to the server
delivered_to_user Message has arrived on the users' phone
read Message has been read by the user (may not be triggered if for example the user has read-receipts turned off)
delivery_failed There was an error, for example the user did not exist on our system

User Data Requests

User data requests are used to request user information from MoyaApp.

Before sending user data requests please request access to user data requests from api-support@moya.app

Permission Required: KYC

Start

User Data requests are started by sending the user number to the user_data/start/ endpoint with the requested data fields in the body:

POST /v1/user_data/start/<USER NUMBER>

Json body:

{
    "reason": "Demo app would like access to your information",
    "data": [
        {
            "property": "first_name",
            "reason": "display on your profile",
            "required": true
        },
        {
            "property": "last_name",
            "reason": "display on your profile",
            "required": false
        }
    ]
}

With a successful request a "request Id" is generated and returned. Please see the example below next.

Example 1: Successful Request

curl --location --request POST 'https://api.moya.app/v1/user_data/start/<USER NUMBER>' \
--header 'Authorization: Bearer YourSecretToken' \
--header 'Content-Type: application/json' \
--data-raw '{
    "reason": "Demo app would like access to your information",
    "data": [
        {
            "property": "first_name",
            "reason": "Your name to display on our profile",
            "required": false
        },
        {
            "property": "last_name",
            "reason": "Your last name to display on our profile",
            "required": false
        }
    ]
}'

Example 1: Successful Response (Status : 202)

{
    "requestId": "11111111-1111-1111-1111-111111111111"
}

Parameters

Name Type Description
reason String (Optional) A reason shown to the user for the data request.
data.property String A property name (see available properties list below).
data.reason String (Optional) A reason for the specific property shown to the user.
data.required Bool Whether the property should be shown as required (greyed out checkbox, user accepts property).

A required property (true value) will be shown to the user with a greyed out checkbox. If the user "Accept" the user data request, required properties will be approved. If a required property is false then the user need to select the field (checked checkbox) to approve a field.

Properties list

Certain properties can be fetched without needing permission from the user. If you are only fetching these, consider using the one-shot fetch mechanism documented below:

Name Example Description
number 27821231234 Phone number of the user, validated by SMS OTP
last_seen 2022-12-31 The date when the user was last seen on our platform
first_seen 2022-05-12 The date when the user was first seen on our platform
did 79228d8f-38e4-47fc-b673-156f12903de6 The device ID
device_platform android The OS that the user is using
app_version 5.4.3 The MoyaApp version the user is on

Most properties require the user to accept the KYC request:

Name Example Description
first_name Bill Name of user.
last_name Smith Last name of user.
date_of_birth 1999-01-01 Date of birth of user in ISO format.
device_make samsung The user's device make
device_model SM-A515F The user's device model code
device_tac 35168418 The user's device TAC
device_os 12 The user's device OS version
display_name BSmith The user's profile name on Moya
id_number 9901011011080 ID Number/Passport of user.
verified_id_number 9901011011080 ID Number/Passport of user having been verified by our system. (Note due to additional cost needs a token with the kyc:fetch:verified_id scope).
id_issue_date 2020-12-31 Date when the ID card/passport was issued
id_expiry_date 2032-12-31 Date when the ID card/passport expires
citizenship ZA ISO country code of citizenship on ID card
moyapay_document_type id id/passport
moyapay_document_country SOUTH_AFRICA Country that issued the document
gender Male User's gender
children 4 The number of children that the user has
email mark@gmail.com The users' email address
ethnicity colored The users ethnicity
work_status Working Details about the users' job
marital_status Living with a partner Details about the users marital status
monthly_income R3000-R10k Income bracket
education_level Lower secondary school usually from 11 - 15 years old User's education level
suburb eThekwini Ward 30 The suburb that the user is in
municipality eThekwini Metropolitan Municipality The municipality that the user is in
city Johannesburg The city that the user is in
province gauteng The region that the user is in
postcode 0123 The users postcode
location_accuracy 10 The rough accuracy of the location data in kilometers, (0 = very accurate)

Fetch

User Data requests are fetched by sending the request id to the user_data/fetch/ endpoint. Results will only be available if the user accepts the user request in MoyaApp.

An optional query parameter named long_poll can be specified if you wish to wait a certain amount of time (up to 60 seconds) if you want the call to hang until the user has responded.

See below for the status list.

GET /v1/user_data/fetch/<REQUEST ID>?long_poll=30

Example Response - Pending (200)

{
    "status": "pending"
}

Example Response - Completed all fields approved (200)

{
    "status": "completed",
    "results": {
        "first_name": {
            "source": "moyapay",
            "value": "NAME OF USER",
            "timestamp": "2022-12-16T16:28:42",
            "status": "approved"
        },
        "last_name": {
            "source": "moyapay",
            "value": "SURNAME OF USER",
            "timestamp": "2022-12-16T16:28:42",
            "status": "approved"
        }
    }
}

Example Response - Completed, one field rejected (200)

{
    "status": "completed",
    "results": {
        "first_name": {
            "status": "rejected"
        },
        "last_name": {
            "source": "moyapay",
            "value": "SURNAME OF USER",
            "timestamp": "2022-12-16T16:28:42",
            "status": "approved"
        }
    }
}

Curl example:

curl 'https://api.moya.app/v1/user_data/fetch/<REQUEST ID>' \
    --header 'Authorization: Bearer YourSecretToken'

Parameters

Name Type Description
Request ID String The request ID generated from the start endpoint

Status list

Name Description
completed User data request was completed by the user.
pending User data request is still pending ( waiting for user to respond).
expired The request has expired and you will need to issue a new one.
approved User approved user data request for field.
rejected User rejected user data request for field.
not_found Request ID or field not found.
invalid_format Request ID invalid format.

Sources

The source parameter says which database in Moya this data came from. We may have different sources of data and these are tried in order of reliability. For example, for date_of_birth we may have a verified date of birth from a MoyaPay application, or something given when signing up to Moya Research, or something which was given when user first signed up to Moya.

Name Reliability Description
moyapay High (validated against ID card) Given when the user applied to MoyaPay and was verified. This is based data that was on the RSA-ID or Passport photo provided in the application process
verified_ids High (validated against ID card) The ID number belongs to the specified user as validated by department of home affairs data.
moya-stats High Reported by the user's device on a daily basis
moya High Moya meta-data during signup process (eg phone number)
gps Medium Sent via the phone's GPS
ip Medium Geolookup based on the user's IP address when interacting with services
loadshedding Medium Sent from the loadshedding app
research Medium Self-reported when user joined the Moya Research panel
proof-of-address Medium Self-reported when user requested proof of address
leadgen Medium Self-reported when user filled in a leadgen form
moyapay-unverified Medium Given when the user applied to MoyaPay, but unlike moyapay has not been verified against government databases.
onboard Low Self-reported when user first joined Moya

Permission Required: KYC

One-shot fetch

If you are fetching user properties which are automatically granted then you can combine the /start and /fetch calls into a single call. The input parameters are the same as the /start, and the response is the same as /fetch:

POST /v1/user_data/fetch/<USER NUMBER>

JSON body:

{
    "data": [
        {
            "property": "app_version",
            "required": true
        }
    ]
}

Example Response - Completed (200)

{
    "status": "completed",
    "results": {
        "app_version": {
            "source": "moya-stats",
            "value": "6.1.2",
            "timestamp": "2023-12-16T16:28:42",
            "status": "approved"
        }
    }
}

Check

You can check if a given set of data points exists for a user without requiring the users consent using this endpoint.

POST /v1/user_data/check/<USER NUMBER>

Json body:

{
    "data": [
        { "property": "first_name" },
        { "property": "last_name" }
    ]
}

Return data looks very similar to the fetch endpoint but does not include the source or value fields.

Example 1: Successful Request

curl --location --request POST 'https://api.moya.app/v1/user_data/start/<USER NUMBER>' \
--header 'Authorization: Bearer YourSecretToken' \
--header 'Content-Type: application/json' \
--data-raw '{
    "data": [
        { "property": "first_name" },
        { "property": "last_name" }
    ]
}'

Example 1: Successful Response (Status: 200)

{
    "requestId": "11111111-1111-1111-1111-111111111111",
    "results": {
        "first_name": {"timestamp": "2023-09-23T09:55:29", "status": "found"},
        "last_name": {"timestamp": "2023-09-23T09:55:29", "status": "found"}
    }
}